• xapian 搜索引擎介绍与使用入门


    Xapian 是一个开源搜索引擎库,使用 C++ 编写,并提供绑定(bindings )以允许从多种编程语言使用。它是一个高度适应性的工具包,允许开发人员轻松地将高级索引和搜索功能添加到自己的应用程序中。Xapian 支持多种加权模型和丰富的布尔查询运算符。最新稳定版本是 1.4.24,发布于 2023 年 11 月 6 日。

    Xapian是20年前就开源的搜索引擎,整体比较稳定,功能层面较lucene有差距,但是足够成熟可用。唯一的缺憾是GPL V2协议。

    安装#

    编译安装core#

    下载最新的tar包,解压并编译安装:

    Copy
    tar xf xapian-core-1.4.24.tar.xz cd xapian-core-1.4.24/ ./configure --prefix=/opt make make install

    安装多语言绑定#

    需要先下载xapian-bindings-1.4.24,然后解压并编译:

    Copy
    tar xf xapian-bindings-1.4.24.tar.xz cd xapian-bindings-1.4.24/ ./configure XAPIAN_CONFIG=/data/xapian-core-1.4.24/xapian-config --with-java --with-python3 make make install
    • configure 时,需要指定XAPIAN_CONFIG的路径,就是上面core里的路径
    • --with-java --with-python3 是只编译java 和 python3的绑定

    使用#

    c++ 使用#

    可以在core目录,新建一个demo目录,新增src/main.cpp

    Copy
    #include #include #include "xapian.h" const std::string index_data_path = "./index_data"; const std::string doc_id1 = "doc1"; const std::string doc_title1 = "如何 构建 搜索引擎 搜索 引擎"; const std::string doc_content1 = "how to build search engine"; const std::string doc_id2 = "doc2"; const std::string doc_title2 = "搜索 是 一个 基本 技能"; const std::string doc_content2 = "search is a basic skill"; const int DOC_ID_FIELD = 101; void build_index() { std::cout << "--- build_index" << std::endl; Xapian::WritableDatabase db(index_data_path, Xapian::DB_CREATE_OR_OPEN); Xapian::TermGenerator indexer; Xapian::Document doc1; doc1.add_value(DOC_ID_FIELD, doc_id1); // custom property doc1.set_data(doc_content1); // payload indexer.set_document(doc1); indexer.index_text(doc_title1); // could use space seperated text line like terms or article db.add_document(doc1); Xapian::Document doc2; doc2.add_value(DOC_ID_FIELD, doc_id2); // custom property doc2.set_data(doc_content2); indexer.set_document(doc2); indexer.index_text(doc_title2); db.add_document(doc2); db.commit(); } void search_op_or() { std::cout << "--- search_op_or" << std::endl; Xapian::Database db(index_data_path); Xapian::Enquire enquire(db); Xapian::QueryParser qp; // std::string query_str = "search engine"; // Xapian::Query query = qp.parse_query(query_str); Xapian::Query term1("搜索"); Xapian::Query term2("引擎"); Xapian::Query query = Xapian::Query(Xapian::Query::OP_OR, term1, term2); std::cout << "query is: " << query.get_description() << std::endl; enquire.set_query(query); Xapian::MSet matches = enquire.get_mset(0, 10); // find top 10 results std::cout << matches.get_matches_estimated() << " results found" << std::endl; std::cout << "matches 1-" << matches.size() << std::endl; for (Xapian::MSetIterator it = matches.begin(); it != matches.end(); ++it) { Xapian::Document doc = it.get_document(); std::string doc_id = doc.get_value(DOC_ID_FIELD); std::cout << "rank: " << it.get_rank() + 1 << ", weight: " << it.get_weight() << ", match_ratio: " << it.get_percent() << "%, match_no: " << *it << ", doc_id: " << doc_id << ", doc content: [" << doc.get_data() << "]\n" << std::endl; } } void search_op_and() { std::cout << "--- search_op_and" << std::endl; Xapian::Database db(index_data_path); Xapian::Enquire enquire(db); Xapian::QueryParser qp; Xapian::Query term1("搜索"); Xapian::Query term2("技能"); Xapian::Query query = Xapian::Query(Xapian::Query::OP_AND, term1, term2); std::cout << "query is: " << query.get_description() << std::endl; enquire.set_query(query); Xapian::MSet matches = enquire.get_mset(0, 10); // find top 10 results, like split page std::cout << matches.get_matches_estimated() << " results found" << std::endl; std::cout << "matches 1-" << matches.size() << std::endl; for (Xapian::MSetIterator it = matches.begin(); it != matches.end(); ++it) { Xapian::Document doc = it.get_document(); std::string doc_id = doc.get_value(DOC_ID_FIELD); std::cout << "rank: " << it.get_rank() + 1 << ", weight: " << it.get_weight() << ", match_ratio: " << it.get_percent() << "%, match_no: " << *it << ", doc_id: " << doc_id << ", doc content: [" << doc.get_data() << "]\n" << std::endl; } } int main(int argc, char** argv) { std::cout << "hello xapian" << std::endl; build_index(); search_op_or(); search_op_and(); return 0; }

    cmake 文件

    Copy
    cmake_minimum_required(VERSION 3.24) project(xapian_demo) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) include_directories( ../include ) link_directories( ../.libs ) file(GLOB SRC src/*.h src/*.cpp ) add_executable(${PROJECT_NAME} ${SRC}) target_link_libraries(${PROJECT_NAME} xapian uuid )

    编译、测试:

    Copy
    #cmake . -- Configuring done -- Generating done -- Build files have been written to: /data/xapian-core-1.4.24/demo #make Consolidate compiler generated dependencies of target xapian_demo [ 50%] Building CXX object CMakeFiles/xapian_demo.dir/src/main.cpp.o [100%] Linking CXX executable xapian_demo [100%] Built target xapian_demo #./xapian_demo hello xapian --- build_index --- search_op_or query is: Query((搜索 OR 引擎)) 2 results found matches 1-2 rank: 1, weight: 0.500775, match_ratio: 100%, match_no: 1, doc_id: doc1, doc content: [how to build search engine] rank: 2, weight: 0.0953102, match_ratio: 19%, match_no: 2, doc_id: doc2, doc content: [search is a basic skill] --- search_op_and query is: Query((搜索 AND 技能)) 1 results found matches 1-1 rank: 1, weight: 0.500775, match_ratio: 100%, match_no: 2, doc_id: doc2, doc content: [search is a basic skill]

    python 使用#

    上面c++的测试仅有几条数据,python我们来上点压力。
    搜索数据源是包含上百万数据的xml,文件里数据格式是给manticore使用的sphinxxml格式:

    Copy
    <sphinx:document id="3669513577616591688"><domain_rank>domain_rank><page_rank>page_rank><author_rank>author_rank><update_ts>update_ts><crawl_ts>crawl_ts><index_ts>index_ts><freq>freq><pv>pv><comment>comment><forward>forward><up>up><title_lac>title_lac><title_jieba>title_jieba><summary_lac>summary_lac><summary_jieba>summary_jieba><url>url><domain>domain><keywords_lac>keywords_lac><image_link>image_link><post_ts>post_ts>sphinx:document>

    因此,我们先编写一个读取程序:

    Copy
    import xmltodict def read_sphinx_xml(file_path): file = open(file_path, 'r', encoding='utf-8') xml_str = '' end_tag = '' for line in file: if end_tag in line: try: xml_str = xml_str + line xml_dict = xmltodict.parse(xml_str) yield xml_dict['sphinx:document'] except Exception as e: print(xml_str) print(e) xml_str = '' else: xml_str = xml_str + line

    然后,调用xapian的binding接口来构建索引:

    Copy
    def list_files(path): return [item for item in os.listdir(path) if ".txt" in item] DOC_ID_FIELD = 101 DOC_TITLE_FIELD = 102 ### Start of example code. def index(datapath, dbpath): # Create or open the database we're going to be writing to. db = xapian.WritableDatabase(dbpath, xapian.DB_CREATE_OR_OPEN) termgenerator = xapian.TermGenerator() count = 0 for file in list_files("/data"): print(f'start load data from {file}') for fields in read_sphinx_xml(f"/data/{file}"): title = fields.get('title_jieba', '') summary = fields.get('summary_jieba', '') identifier = fields.get('@id', '') if summary is None: summary = '' if title is None: continue count = count + 1 doc = xapian.Document() termgenerator.set_document(doc) # title 放大5倍 termgenerator.index_text(title * 5 + ' ' + summary) # 存入数据 doc.add_value(DOC_ID_FIELD, identifier) doc.add_value(DOC_TITLE_FIELD, title) doc.set_data(identifier + ' ' + title) # indexer. idterm = u"Q" + identifier doc.add_boolean_term(idterm) db.replace_document(idterm, doc) if count % 10000 == 0: print(f'loaded {count}')

    注意:

    • xapian对字段支持的不够好,需要用suffix实现,这里测试就将title放大5倍混合summary进行建立索引
    • doc.add_value 可以存储字段值,后续可以doc.get_value读取
    • doc.set_data 可以用来存储doc的完整信息,方便显示,doc信息会存储在独立的doc文件中
    • 这里add_boolean_term和replace_document,可以实现相同id的数据覆盖

    下面来看查询

    Copy
    #!/usr/bin/env python import json import sys import xapian import support import time def search(dbpath, querystring, offset=0, pagesize=10): # offset - defines starting point within result set # pagesize - defines number of records to retrieve # Open the database we're going to search. db = xapian.Database(dbpath) # Set up a QueryParser with a stemmer and suitable prefixes queryparser = xapian.QueryParser() query = queryparser.parse_query(querystring) print(query) # Use an Enquire object on the database to run the query enquire = xapian.Enquire(db) enquire.set_query(query) start_time = time.time() # And print out something about each match matches = [] for match in enquire.get_mset(offset, pagesize): print(f'rank: {match.rank} weight: {match.weight} docid: {match.document.get_value(101).decode("utf-8")} title: {match.document.get_value(102).decode("utf-8")}') # print(match.document.get_data().decode('utf8')) matches.append(match.docid) print(f'cost time {1000 * (time.time() - start_time)}ms') # Finally, make sure we log the query and displayed results support.log_matches(querystring, offset, pagesize, matches) if len(sys.argv) < 3: print("Usage: %s DBPATH QUERYTERM..." % sys.argv[0]) sys.exit(1) search(dbpath = sys.argv[1], querystring = " ".join(sys.argv[2:]))

    解释:

    • xapian.QueryParser() 可以解析查询query,可以使用+ -,默认是or`查询
    • 依然通过xapian.Enquire对象查询,通过get_mset获取结果
    • doc可以通过document.get_value读取存储的字段值,可以通过get_data读取存储的doc信息,要显示需要先decode('utf8')

    下面来测试查询,在已构建的330万+索引数据上,搜索 21 世纪 十大 奇迹 都 有 哪些

    默认的or查询,耗时46ms:

    Copy
    (base) [root@dev demo]#python3 py_search.py ./test_index_2/ '21 世纪 十大 奇迹 都 有 哪些' Query((21@1 OR 世纪@2 OR 十大@3 OR 奇迹@4 OR 都@5 OR 有@6 OR 哪些@7)) rank: 0 weight: 36.96501079176272 docid: 270926605591973127 title: 21 世纪 的 十大 奇迹 ( 王金宝 ) rank: 1 weight: 26.66735387825444 docid: 1202595084889677840 title: 淮安 十大 装修 公司 排行榜 都 有 哪些 rank: 2 weight: 26.637435058757113 docid: 4515279401098254828 title: 十大 轻奢 首饰 品牌 耳环 ( 十大 轻奢 首饰 品牌 耳环 排名 ) rank: 3 weight: 25.896035383457647 docid: 2734857435606641662 title: 中国 十大 奇迹 都 是 什么 rank: 4 weight: 25.705459264178575 docid: 7786914994161493217 title: 每个 民族 都 有 伤痕 和 血泪 ( 二 ) , 再说 说 曾经 创造 奇迹 的 蒙古 帝国 ! rank: 5 weight: 25.5095343276925 docid: 1500823194476917788 title: 真正 复古 的 奇迹 手游安卓 下载 2022 十大 真正 复古 的 奇迹 手游 推荐 ... rank: 6 weight: 25.47914915723924 docid: 868651613852701914 title: 21 世纪 有 哪些 著名 的 科学家 有 哪些 ? 急 ? rank: 7 weight: 25.41860730241055 docid: 7128947999947583631 title: 西安 临潼区 必玩 十大 景区 , 西安 临潼区 有 哪些 景点 推荐 、 旅游 ... rank: 8 weight: 25.16026635261191 docid: 6074515952166234396 title: 世界 建筑史 上 堪称 逆天 的 十大 工程 , 个个 都 是 奇迹 ! rank: 9 weight: 24.89609264689645 docid: 5578567283356182005 title: 20 世纪 的 科技 发明 有 哪些 20 世纪 有 哪些 重大 科学 发现 和 科学 ... cost time 46.19002342224121ms '21 世纪 十大 奇迹 都 有 哪些'[0:10] = 461487 2291460 457410 1416736 3245773 1156355 3030607 2498966 2025338 254698

    如何优化查询耗时呢,我们可以先预测,这里 十大 奇迹 是核心词,我们可以要求必出,因此查询串可以变为: 21 世纪 +十大 +奇迹 都 有 哪些

    Copy
    (base) [root@dev demo]#python3 py_search.py ./test_index_2/ '21 世纪 +十大 +奇迹 都 有 哪些' Query(((十大@3 AND 奇迹@4) AND_MAYBE (21@1 OR 世纪@2 OR (都@5 OR 有@6 OR 哪些@7)))) rank: 0 weight: 36.96293887882541 docid: 270926605591973127 title: 21 世纪 的 十大 奇迹 ( 王金宝 ) rank: 1 weight: 25.89233097995836 docid: 2734857435606641662 title: 中国 十大 奇迹 都 是 什么 rank: 2 weight: 25.505700206213298 docid: 1500823194476917788 title: 真正 复古 的 奇迹 手游安卓 下载 2022 十大 真正 复古 的 奇迹 手游 推荐 ... rank: 3 weight: 25.41629259671702 docid: 7128947999947583631 title: 西安 临潼区 必玩 十大 景区 , 西安 临潼区 有 哪些 景点 推荐 、 旅游 ... rank: 4 weight: 25.156904086936752 docid: 6074515952166234396 title: 世界 建筑史 上 堪称 逆天 的 十大 工程 , 个个 都 是 奇迹 ! rank: 5 weight: 24.62510506307912 docid: 193253728534326320 title: 十大 凶梦有 哪些 ? 十大 凶梦 列表 ! 观音 灵签 算命网 rank: 6 weight: 23.192754028779266 docid: 7179285817750982899 title: 十大 电脑 恐怖 游戏 排行 好玩 的 恐怖 游戏 有 哪些 rank: 7 weight: 23.14557703440898 docid: 8499116988738957144 title: 十大 爆火 的 奇迹 类手游 排行榜 最火 的 奇迹 类手游 排名 前十 特 ... rank: 8 weight: 22.274870321417836 docid: 1134007698166133600 title: 世界 十大 著名 建筑物 感受 人类 的 辉煌 奇迹 建筑 第一 排行榜 rank: 9 weight: 22.214192030795594 docid: 7678030174605825797 title: 世界 十大 奇迹 动物 : 爱尔兰 大鹿 死而复生 世界 十大 建筑 奇迹 cost time 2.651214599609375ms '21 世纪 +十大 +奇迹 都 有 哪些'[0:10] = 461487 1416736 1156355 2498966 2025338 173861 448901 723659 1029533 1830781

    耗时3ms不到,且结果更优质。

    总结#

    xapian的介绍到这里告一段落,后续文章会深入xapian的内部细节。

    关注作者

    欢迎关注作者微信公众号, 一起交流软件开发:欢迎关注作者微信公众号

  • 相关阅读:
    【FPGA教程案例11】基于vivado核的除法器设计与实现
    linux 安装jdk
    JAVA设计模式-代理模式
    Elasticsearch模拟网络丢包
    JAVA:实现Disjoint Sets不相交集合算法(附完整源码)
    Python动态建模(2)
    Facebook社区:小组功能如何改变社交互动
    LeetCode算法题整理(200题左右)
    HC32L110(五) Ubuntu20.04 VSCode的Debug环境配置
    关于MATLAB中双精度值的解释,以及Inf,NaN的值具体是多少,为什么是这个值
  • 原文地址:https://www.cnblogs.com/xiaoqi/p/17972303/Xapian