2024年1月

从零开始

在本文中,我们将详细介绍如何在Python / pyspark环境中使用graphx进行图计算。GraphX是Spark提供的图计算API,它提供了一套强大的工具,用于处理和分析大规模的图数据。通过结合Python / pyspark和graphx,您可以轻松地进行图分析和处理。

为了方便那些刚入门的新手,包括我自己在内,我们将从零开始逐步讲解。

安装Spark和pyspark

如果你只是想单独运行一下pyspark的演示示例,那么只需要拥有Python环境就可以了。你可以前往官方网站的快速开始页面查看详细的指南:
https://spark.apache.org/docs/latest/api/python/getting_started/quickstart_df.html

安装pyspark包

pip install pyspark

由于官方省略的步骤还是相当多的,我简单写了一下我的成功演示示例。

from pyspark.sql import SparkSession,Row
from datetime import datetime, date
import pandas as pd
import os
os.environ['PYSPARK_PYTHON'] = "%你的Python包路径%//python.exe"

spark = SparkSession.builder.getOrCreate()
df = spark.createDataFrame([
    Row(a=1, b=2., c='string1', d=date(2000, 1, 1), e=datetime(2000, 1, 1, 12, 0)),
    Row(a=2, b=3., c='string2', d=date(2000, 2, 1), e=datetime(2000, 1, 2, 12, 0)),
    Row(a=4, b=5., c='string3', d=date(2000, 3, 1), e=datetime(2000, 1, 3, 12, 0))
])
df.show()

然而,考虑到我们今天需要使用GraphX进行分析,因此我们仍然需要安装Spark。

安装Spark

请访问Spark官方网站(
https://spark.apache.org/downloads.html)以获取适用于您操作系统的最新版本,并进行下载。如果您觉得下载速度较慢,您还可以选择使用国内阿里镜像进行下载。为了方便起见,我已经帮您找到了相应的镜像地址。

国内阿里镜像:
https://mirrors.aliyun.com/apache/spark/spark-3.5.0/?spm=a2c6h.25603864.0.0.52d72104qIXCsH

请下载带有hadoop的版本:
spark-3.5.0-bin-hadoop3.tgz
。解压缩Spark压缩包即可

配置环境变量

在安装Spark之前,请务必记住需要Java环境。请确保提前配置好JAVA_HOME环境变量,这样才能正常运行Spark。

在windows上安装Java和Apache Spark后,设置SPARK_HOME、HADOOP_HOME和PATH环境变量。如果你知道如何在windows上设置环境变量,请添加以下内容:

SPARK_HOME  = C:\apps\opt\spark-3.5.0-bin-hadoop3
HADOOP_HOME = C:\apps\opt\spark-3.5.0-bin-hadoop3

image

在Windows上使用winutils.exe的Spark

在Windows上运行Apache Spark时,确保你已经下载了适用于Spark版本的winutils.exe。winutils.exe是一个用于在Windows环境下模拟类似POSIX的文件访问操作的工具,它使得Spark能够在Windows上使用Windows特有的服务和运行shell命令。

你可以从以下链接下载适用于你所使用的Spark版本的winutils.exe:
https://github.com/kontext-tech/winutils/tree/master/hadoop-3.3.0/bin

请确保将下载的winutils.exe文件放置在Spark安装目录的bin文件夹下,以便Spark能够正确地使用它来执行Windows特有的操作。

Apache Spark shell

spark-shell是Apache Spark发行版附带的命令行界面(CLI)工具,它可以通过直接双击或使用命令行窗口在Windows操作系统上运行。此外,Spark还提供了一个Web UI界面,用于在Windows上进行可视化监控和管理。

image

请尝试运行Apache Spark shell。当你成功运行后,你应该会看到一些内容输出(请忽略最后可能出现的警告信息)。

image

在启动Spark-shell时,它会自动创建一个Spark上下文的Web UI。您可以通过从浏览器中打开URL,访问Spark Web UI来监控您的工作。

image

GraphFrames

在前面的步骤中,我们已经完成了所有基础设施(环境变量)的配置。现在,我们需要进行一些配置来使Python脚本能够运行graphx。

要使用Python / pyspark运行graphx,你需要进行一些配置。接下来的示例将展示如何配置Python脚本来运行graphx。

GraphFrames的安装

如需获得更多关于GraphFrames的信息和快速入门指南,请访问官方网站:
https://graphframes.github.io/graphframes/docs/_site/quick-start.html。

你也可以使用以下命令来安装GraphFrames。

pip install graphframes

在继续操作之前,请务必将graphframes对应的jar包安装到spark的jars目录中,以避免在使用graphframes时出现以下错误:
java.lang.ClassNotFoundException: org.graphframes.GraphFramePythonAPI

image

将下载好的jar包放入你的%SPARK_HOME%\jars即可。

image

接下来,我们可以开始正常地使用graphx图计算框架了。现在,让我们简单地浏览一下一个示例demo。

from pyspark.sql.types import *
from pyspark.sql import SparkSession
from pyspark import SparkContext, SparkConf
import pandas as pd
from graphframes import GraphFrame

spark_conf = SparkConf().setAppName('Python_Spark_WordCount').setMaster('local[2]') 
sc = SparkContext(conf=spark_conf)
spark=SparkSession.builder.appName("graph").getOrCreate()
v = spark.createDataFrame([
  ("a", "Alice", 34),
  ("b", "Bob", 36),
  ("c", "Charlie", 30),
], ["id", "name", "age"])

# Create an Edge DataFrame with "src" and "dst" columns
e = spark.createDataFrame([
  ("a", "b", "friend"),
  ("b", "c", "follow"),
  ("c", "b", "follow"),
], ["src", "dst", "relationship"])
# Create a GraphFrame
g = GraphFrame(v, e)

# Query: Get in-degree of each vertex.
g.inDegrees.show()

# Query: Count the number of "follow" connections in the graph.
g.edges.filter("relationship = 'follow'").count()

# Run PageRank algorithm, and show results.
results = g.pageRank(resetProbability=0.01, maxIter=20)
results.vertices.select("id", "pagerank").show()

如果运行还是报错:org.apache.spark.SparkException: Python worker failed to connect back

import os
os.environ['PYSPARK_PYTHON'] = "%你自己的Python路径%//Python//python.exe"

最后大功告成:

image

网络流量分析

接下来,我们将探讨一下是否能够对网络流量进行分析。对于初学者来说,很难获得一些有组织的日志文件或数据集,所以我们可以自己制造一些虚拟数据,以便进行演示。

首先,让我来详细介绍一下GraphFrame(v, e)的参数:

参数v:Class,这是一个保存顶点信息的DataFrame。DataFrame必须包含名为"id"的列,该列存储唯一的顶点ID。

参数e:Class,这是一个保存边缘信息的DataFrame。DataFrame必须包含两列,"src"和"dst",分别用于存储边的源顶点ID和目标顶点ID。

edges=sc.textFile(r'/Users/xiaoyu/edges')
edges=edges.map(lambda x:x.split('\t'))
edges_df=spark.createDataFrame(edges,['src','dst'])

nodes=sc.textFile(r'/Users/xiaoyu/nodes')
nodes=nodes.map(lambda x:[x]) 
nodes_df=spark.createDataFrame(nodes,['id'])
graph=GraphFrame(nodes_df, edges_df)

为了创建图数据结构并进行分析,可以简化流程,直接读取相关文件并进行处理。

# 计算每个节点的入度和出度
in_degrees = graph.inDegrees
out_degrees = graph.outDegrees

# 打印节点的入度和出度
in_degrees.show()
out_degrees.show()

查找具有最大入度和出度的节点:

# 找到具有最大入度的节点
max_in_degree = in_degrees.agg(F.max("inDegree")).head()[0]
node_with_max_in_degree = in_degrees.filter(in_degrees.inDegree == max_in_degree).select("id")

# 找到具有最大出度的节点
max_out_degree = out_degrees.agg(F.max("outDegree")).head()[0]
node_with_max_out_degree = out_degrees.filter(out_degrees.outDegree == max_out_degree).select("id")

# 打印结果
node_with_max_in_degree.show()
node_with_max_out_degree.show()

总结

本文介绍了如何在Python / pyspark环境中使用graphx进行图计算。通过结合Python / pyspark和graphx,可以轻松进行图分析和处理。首先需要安装Spark和pyspark包,然后配置环境变量。接着介绍了GraphFrames的安装和使用,包括创建图数据结构、计算节点的入度和出度,以及查找具有最大入度和出度的节点。最后,希望本文章对于新手来说有一些帮助~


SetFitABSA 是一种可以有效从文本中检测方面级情感的技术。

方面级情感分析 (Aspect-Based Sentiment Analysis,ABSA) 是一种检测文本中特定方面的情感的任务。例如,在“这款手机的屏幕很棒,但电池太小”一句中,分别有“屏幕”和“电池”两个方面,它们的情感极性分别是正面和负面。

ABSA 应用颇为广泛,有了它我们可以通过分析顾客对产品或服务的多方面反馈,并从中提取出有价值的见解。然而,ABSA 要求在样本标注时对训练样本中涉及的各个方面进行词元级的识别,因此为 ABSA 标注训练数据成为了一件繁琐的任务。

为了缓解这一问题,英特尔实验室和 Hugging Face 联袂推出了 SetFitABSA,以用于少样本场景下的特定领域 ABSA 模型训练。实验表明,SetFitABSA 性能相当不错,其在少样本场景下表现甚至优于 Llama2 和 T5 等大型生成模型。

与基于 LLM 的方法相比,SetFitABSA 有两个独特优势:

在我们开发Winform界面的时候,有时候会遇到需要对一些字段进行一些汇总的管理,如果在列表中能够对表格列表中的内容进行分组展示,将比较符合我们的预期,本篇随笔介绍在Winform开发中如何利用DevExpress的GridView实现该功能。

1、准备数据表和一些数据

我们来以ERP系统中的一个原料表格来介绍我们对表格数据汇总的处理,首先创建一个原料表用来测试,表的数据库设计可以采用PowerDesigner等类似的建模工具设计,如下所示。

通过预览生成SQL的方式,我们可以生成对应不同的数据库的数据库脚本,用于创建对应的数据库表。

生成对应的数据表后,我们往里面填入一些测试的数据,用于实际的测试处理。

我们准备类别不同的数据写入,为了方便测试,具体数据效果如下所示。

有了这些,我们就可以顺利进行下面的代码开发了。

2、基于具体框架生成基础数据库操作类

在这里可以根据我们实际的需要,生成相关的基础数据库操作类,如我的Winform框架或者SqlSugar 开发框架的Winform前端,都可以直接生成对应的框架基础代码,我们这里以我们的SqlSugar 开发框架模式来开发对应的Winform界面。

由于我们开发一般是基于增量式的开发方式,因此我们可以一次性选择我们新增的表进行生成底层框架的支持代码,如下确定界面所示。

最终生成一个可以直接运行的项目整体框架方案。

如果我们是考虑增量式的整合在我们现有的项目上,那么我们可以把SugarProjectCore项目目录里面的文件夹整个复制整合到我们已有的项目上即可。

它们的目录结构已经根据不同的功能放在了不同的文件夹中了,如下图所示。

例如,我们把它们整个复制放置到了对应项目目录下,如下所示。

那么目前我们已有项目上就增加了对应的访问数据库的业务类所需要的模型、接口、具体实现类了。

接下来就是对Winform的界面进行生成了。

3、生成相关的界面,并实现分组的特殊处理

前面我们介绍了,这个案例介绍基于我们SqlSugar开发框架的实现,因此我们基于SqlSugar开发框架的Winform界面进行生成即可。生成的界面直接调用刚才的创建的基础接口来请求数据和绑定处理,以及相关的导入导出、编辑、删除等基础操作。

上面的界面就是基于SqlSugar开发框架生成的Winform界面的时候,对界面中查询条件、列表展示的字段,以及编辑界面的字段进行设计的处理,代码生成工具根据这些设定进行界面代码的生成。

生成的代码,自动包含了列表展示和编辑等基础界面功能,如下代码文件所示。

我们把这些文件整合到我们已有的UI项目上,就可以实现表的界面代码增量的生成了。

直接测试编译运行,稍作调整,最终的界面效果如下所示。

以上的表格数据,只是常规的展示,并没有实现分组的处理,我们增加一个复选框,用来设置分组处理的展示,如下界面所示。

我们注意到,在WInform界面中,生成的界面代码包含了对数据的绑定显示,如下就是对表的数据请求函数。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<MaterialInfo>>GetData()
{
MaterialPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new MaterialPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else{//构建分页的条件和查询条件 pagerDto = new MaterialPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 Category = this.txtCategory.Text.Trim(),
Code
= this.txtCode.Text.Trim(),
Name
= this.txtName.Text.Trim(),
Alias
= this.txtAlias.Text.Trim(),
UsePurpose
= this.txtUsePurpose.Text.Trim(),
SupplierSimple
= this.txtSupplierSimple.Text.Trim(),
Producer
= this.txtProducer.Text.Trim(),
};
//日期和数值范围定义 }var result = await BLLFactory<IMaterialService>.Instance.GetListAsync(pagerDto);returnresult;
}

以上就是根据界面的条件进行数据的条件查询,并返回记录集合。

在获得记录集合后,我们把它赋值给我们自定义的分页控件组件,就可以实现相关数据的正常展示了。

//获取分页数据列表
var result = awaitGetData();//设置所有记录数和列表数据源
this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息
this.winGridViewPager1.DataSource = result.Items;

为了实现分组的处理展示,我们需要判断上面的复选框条件,进行分组条件和表达式的创建处理即可,如下代码所示。

//增加汇总字段和显示
var gridView1 = this.winGridViewPager1.gridView1;if(checkGroup.Checked)
{
this.winGridViewPager1.ShowLineNumber = false;
gridView1.IndicatorWidth
= 0;
gridView1.OptionsView.ShowGroupExpandCollapseButtons
= true;//显示折叠的分组 gridView1.OptionsView.AllowCellMerge = true; //允许合并字段 gridView1.OptionsView.GroupDrawMode =GroupDrawMode.Standard;

gridView1.GroupSummary.Clear();
gridView1.Columns[
"Category"].GroupIndex = 0;//对类别进行分组展示 var item = newGridGroupSummaryItem();
item.FieldName
= "Id";
item.DisplayFormat
= "(合计数量 = {0:n})";
item.SummaryType
= DevExpress.Data.SummaryItemType.Count;//Sum、Average等 gridView1.GroupSummary.Add(item);
gridView1.ExpandAllGroups();
}
else{
gridView1.GroupSummary.Clear();
this.winGridViewPager1.ShowLineNumber = true;
gridView1.OptionsView.AllowCellMerge
= false;
}

以上就是对数据记录进行分组的代码,核心代码就几行,可以很容易实现我们所需要的分组效果,如下界面所示。

如果我们不需要合并,取消设置为false即可。

gridView1.OptionsView.AllowCellMerge = false; 

以上就是我们介绍基于SqlSugar开发框架生成基础代码、界面代码、以及实现分组处理展示的一些总结。

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

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

安装

编译安装core

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

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,然后解压并编译:

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

#include <iostream>
#include <string>
#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 文件

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
)

编译、测试:

#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格式:

<sphinx:document id="3669513577616591688"><domain_rank><![CDATA[0]]></domain_rank><page_rank><![CDATA[0]]></page_rank><author_rank><![CDATA[0]]></author_rank><update_ts><![CDATA[1671120000000]]></update_ts><crawl_ts><![CDATA[1702765056760]]></crawl_ts><index_ts><![CDATA[1703141806692]]></index_ts><freq><![CDATA[0]]></freq><pv><![CDATA[0]]></pv><comment><![CDATA[0]]></comment><forward><![CDATA[0]]></forward><up><![CDATA[0]]></up><title_lac><![CDATA[南充市 首席 风水 大师   罗 李华   百科 词典]]></title_lac><title_jieba><![CDATA[南充市 首席 风水 大师   罗李华   百科词典]]></title_jieba><summary_lac><![CDATA[百科 词典 , 主要 收录 知名 人物 、 企业 、 行业 相关 词条 为主 , 是 由 各 大网民 申请 供稿 , 由 专职 人员 严格 审核 编辑 而成 , 力求 做到 每一个 词条 权威 、 真实 、 客观 、 专业 , 旨在 打造 一个 值得 大家 信赖 的 权威 百科 平台 。]]></summary_lac><summary_jieba><![CDATA[百科词典 , 主要 收录 知名 人物 、 企业 、 行业 相关 词条 为主 , 是 由 各大 网民 申请 供稿 , 由 专职人员 严格 审核 编辑 而成 , 力求 做到 每 一个 词条 权威 、 真实 、 客观 、 专业 , 旨在 打造 一个 值得 大家 信赖 的 权威 百科 平台 。]]></summary_jieba><url><![CDATA[https://www.baikecidian.cn/h-nd-9709.html]]></url><domain><![CDATA[www.baikecidian.cn]]></domain><keywords_lac><![CDATA[]]></keywords_lac><image_link><![CDATA[0]]></image_link><post_ts><![CDATA[1538215160000]]></post_ts></sphinx:document>

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

import xmltodict

def read_sphinx_xml(file_path):
    file = open(file_path, 'r', encoding='utf-8')

    xml_str = ''
    end_tag = '</sphinx:document>'
    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接口来构建索引:

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的数据覆盖

下面来看查询

#!/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:

(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 世纪 +十大 +奇迹 都 有 哪些

(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的内部细节。