在Python中构建有效的稀疏关键字索引
#showdev #python #machinelearning #nlp

语义搜索是基于自然语言处理(NLP)最新进展的新类别。传统搜索系统使用关键字查找数据。语义搜索对自然语言有了解,并确定具有相同含义的结果,不一定是相同的关键字。

虽然语义搜索增加了惊人的功能,但稀疏关键字索引仍然可以增加值。在某些情况下,找到确切匹配很重要,或者我们只希望快速索引快速进行数据集的初始扫描。

不幸的是,对于本地基于Python的关键字索引库,没有很多很棒的选择。大多数可用的选项都不扩展和/或高效,仅专为简单情况而设计。

鉴于Python是一种解释的语言,从性能的角度来看,它通常会出现不良的说唱。在某些情况下,它是合理的,因为Python可能是饥饿的内存,并且具有迫使单线执行的全局解释器锁(GIL)。但是可以与其他语言相构建表演python。

本文将探讨如何在Python中构建有效的稀疏关键字索引,并将结果与​​其他方法进行比较。

安装依赖项

安装txtai和所有依赖项。

# Install txtai
pip install txtai pytrec_eval rank-bm25 elasticsearch==7.10.1

引入问题

在高级别,关键字索引通过将文本纳入每个文档的令牌列表来工作。这些令牌汇总为每个文档频率,并以期限稀疏阵列存储。

术语频率阵列稀疏,因为它们仅在文档中存在令牌时存储频率。例如,如果在1000个文档中的1个中存在一个令牌,则稀疏数组只有一个条目。一个密集的阵列存储1000个条目,除了一个。

一种存储python中的术语频率稀疏阵列的简单方法是每个令牌的{id: frequency}字典。这种方法的问题在于,Python在开销上都有重要的对象。

让我们检查用于单个数字的大小。

import sys

a = 100
sys.getsizeof(a)
28

28个字节,用于一个整数。与4或8个字节的本地int/long相比,这是非常浪费的。想象一下有成千上万的id: frequency映射。内存使用率将快速增长。

让我们演示。下面的代码运行一个自我包含的Python进程,该过程创建了1000万个数字的列表。

作为单独的过程运行有助于计算更准确的内存使用统计。

import psutil

results = []
for x in range(int(1e7)):
  results.append(x)

print(f"MEMORY USAGE = {psutil.Process().memory_info().rss / (1024 * 1024)} MB")
MEMORY USAGE = 394.640625 MB

该数组使用了大约395 MB的内存。这似乎很高。

Python中有效的数字阵列

幸运的是,Python有一个用于构建efficient arrays of numeric values的模块。该模块启用具有相同本地类型的建筑阵列。

让我们尝试使用long long类型进行8个字节。

from array import array

import psutil

results = array("q")
for x in range(int(1e7)):
  results.append(x)

print(f"MEMORY USAGE = {psutil.Process().memory_info().rss / (1024 * 1024)} MB")
MEMORY USAGE = 88.54296875 MB

正如我们所看到的,内存使用率从395 MB升至89 MB。这是一个降低的4倍,与较早的28个字节/数字vs 8字节/数字相符。

有效处理数字数据

纯Python中的大型计算也可能会慢慢慢。幸运的是,数字处理的选项有强大的景观。最受欢迎的框架是NumPy。还有PyTorch和其他基于GPU的张量处理框架。

下面是一个简单的示例,该示例与python vs numpy中的数组进行了分类。

import random
import time

data = [random.randint(1, 500) for x in range(1000000)]

start = time.time()
sorted(data, reverse=True)
print(time.time() - start)
0.33922290802001953
import numpy as np

data = np.array(data)

start = time.time()
np.sort(data)[::-1]
print(time.time() - start)
0.10296249389648438

正如我们所看到的,在numpy中对数组进行排序的速度明显更快。这似乎不是很多

TXTAI中的稀疏关键字索引

现在我们已经讨论了关键绩效概念,让我们谈谈如何将其应用于构建稀疏关键字索引。

回到原始频率稀疏阵列的原始方法,我们看到使用Python数组软件包更有效。在TXTAI中,此方法用于为每个令牌构建术语频率阵列。这会导致本地速度和内存使用量。

搜索方法使用多种Numpy方法来有效计算查询项匹配。每个查询都是令牌化的,并检索那些令牌项频率阵列以计算查询分数。这些数字方法全部写在C中,通常会丢弃GIL。因此,再次,接近天然速度和使用多线程的能力。

阅读full implementation on GitHub以了解更多信息。

评估性能

首先,对景观的评论。如引言中所述,没有很多好的选择。从速度,性能和功能角度来看,Apache Lucene是迄今为止最好的传统搜索指数。这是Elasticsearch/Opensearch和许多其他项目的基础。但这需要Java。

这是我们将要探索的选项。

  • Rank-BM25项目,搜索python bm25时的首要结果。

  • SQLite FTS5扩展。此扩展名在SQLite中构建一个稀疏的关键字索引。

我们将使用Beir数据集。我们还将使用TXTAI项目中的benchmarks script。此基准测试脚本具有与Beir DataSet合作的方法。

基准脚本上的重要警告。

  • 对于SQLite FTS实现,每个令牌与OR子句一起连接在一起。默认情况下,sqlite fts implicitly joins clauses together带有AND条款。相比之下,Lucene's default operatorOR
  • Elasticsearch实现使用7.x,因为在笔记本中实例化更简单。
  • 除Elasticsearch以外的所有方法都使用TXTAI的unicode tokenizer来使文本具有一致性
import os

# Get benchmarks script
os.system("wget https://raw.githubusercontent.com/neuml/txtai/master/examples/benchmarks.py")

# Create output directory
os.makedirs("beir", exist_ok=True)

# Download subset of BEIR datasets
datasets = ["trec-covid", "nfcorpus", "webis-touche2020", "scidocs", "scifact"]
for dataset in datasets:
  url = f"https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/{dataset}.zip"
  os.system(f"wget {url}")
  os.system(f"mv {dataset}.zip beir")
  os.system(f"unzip -d beir beir/{dataset}.zip")

  # Remove existing benchmark data
if os.path.exists("benchmarks.json"):
  os.remove("benchmarks.json")

现在让我们运行基准。

# Remove existing benchmark data
if os.path.exists("benchmarks.json"):
  os.remove("benchmarks.json")

# Runs benchmark evaluation
def evaluate(method):
  for dataset in datasets:
    command = f"python benchmarks.py beir {dataset} {method}"
    print(command)
    os.system(command)

# Calculate benchmarks
for method in ["bm25", "rank", "sqlite"]:
  evaluate(method)
import json
import pandas as pd

def benchmarks():
  # Read JSON lines data
  with open("benchmarks.json") as f:
    data = f.read()

  df = pd.read_json(data, lines=True).sort_values(by=["source", "search"])
  return df[["source", "method", "index", "memory", "search", "ndcg_cut_10", "map_cut_10", "recall_10", "P_10"]].reset_index(drop=True)

# Load benchmarks dataframe
df = benchmarks()
df[df.source == "trec-covid"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Trec-Covid BM25 101.96 997 0.28 0.58119 0.01247 0.01545 0.618
Trec-Covid sqlite 60.16 880 23.09 0.56778 0.01190 0.01519 0.610
Trec-Covid 等级 61.75 3245 75.49 0.57773 0.01210 0.01550 0.632
df[df.source == "nfcorpus"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
nfcorpus BM25 2.64 648 1.08 0.30639 0.11728 0.14891 0.21734
nfcorpus sqlite 1.50 630 12.73 0.30695 0.11785 0.14871 0.21641
nfcorpus 等级 2.75 700 23.78 0.30692 0.11711 0.15320 0.21889
df[df.source == "webis-touche2020"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Webis-toch2020 BM25 374.66 1137 0.37 0.36920 0.14588 0.22736 0.34694
Webis-toch2020 sqlite 220.46 1416 34.61 0.37194 0.14812 0.22890 0.35102
Webis-toch2020 等级 224.07 10347 81.22 0.39861 0.16492 0.23770 0.36122
df[df.source == "scidocs"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Scidocs BM25 17.95 717 1.64 0.15063 0.08756 0.15637 0.0772
Scidocs sqlite 17.85 670 56.64 0.15156 0.08822 0.15717 0.0776
Scidocs 等级 13.11 1056 162.99 0.14932 0.08670 0.15408 0.0761
df[df.source == "scifact"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
scifact BM25 5.51 653 1.07 0.66324 0.61764 0.78761 0.087
scifact sqlite 1.85 631 20.28 0.66630 0.61966 0.79494 0.088
scifact 等级 1.85 724 42.22 0.65618 0.61204 0.77400 0.085

上面的部分显示了每个源和方法的指标。

表标头列出了source (dataset)index methodindex time(s)memory usage(MB)search time(s)NDCG@10/MAP@10/RECALL@10/RECALL@10/P@10精度指标。表由search time排序。

正如我们所看到的,TXTAI的实现总体上是最快的搜索时间。但是,在索引时间方面速度较慢。准确度指标略有不同,但每种方法都大致相同。

内存用法脱颖而出。 Sqlite和Txtai的每个源都有相同的用法。 RANK-BM25内存使用情况可能会迅速失控。例如,webis-touch2020(仅为400K记录)与700 MB相比,使用10 GB的其他实现。

与Elasticsearch进行比较

现在,我们已经审查了在Python中构建关键字索引的方法,让我们看看TXTAI的稀疏关键字索引与Elasticsearch的比较。

我们将旋转一个内联实例并运行相同的评估。

# Download and extract elasticsearch
os.system("wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.1-linux-x86_64.tar.gz")
os.system("tar -xzf elasticsearch-7.10.1-linux-x86_64.tar.gz")
os.system("chown -R daemon:daemon elasticsearch-7.10.1")
from subprocess import Popen, PIPE, STDOUT

# Start and wait for server
server = Popen(['elasticsearch-7.10.1/bin/elasticsearch'], stdout=PIPE, stderr=STDOUT, preexec_fn=lambda: os.setuid(1))
# Add benchmark evaluations for Elasticsearch
evaluate("es")

# Reload benchmarks dataframe
df = benchmarks()
df[df.source == "trec-covid"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Trec-Covid BM25 101.96 997 0.28 0.58119 0.01247 0.01545 0.618
Trec-Covid ES 71.24 636 2.09 0.59215 0.01261 0.01590 0.636
Trec-Covid sqlite 60.16 880 23.09 0.56778 0.01190 0.01519 0.610
Trec-Covid 等级 61.75 3245 75.49 0.57773 0.01210 0.01550 0.632
df[df.source == "nfcorpus"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
nfcorpus BM25 2.64 648 1.08 0.30639 0.11728 0.14891 0.21734
nfcorpus ES 3.95 627 11.47 0.30676 0.11761 0.14894 0.21610
nfcorpus sqlite 1.50 630 12.73 0.30695 0.11785 0.14871 0.21641
nfcorpus 等级 2.75 700 23.78 0.30692 0.11711 0.15320 0.21889
df[df.source == "webis-touche2020"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Webis-toch2020 BM25 374.66 1137 0.37 0.36920 0.14588 0.22736 0.34694
Webis-toch2020 ES 168.28 629 0.62 0.37519 0.14819 0.22889 0.35102
Webis-toch2020 sqlite 220.46 1416 34.61 0.37194 0.14812 0.22890 0.35102
Webis-toch2020 等级 224.07 10347 81.22 0.39861 0.16492 0.23770 0.36122
df[df.source == "scidocs"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
Scidocs BM25 17.95 717 1.64 0.15063 0.08756 0.15637 0.0772
Scidocs ES 11.07 632 10.25 0.14924 0.08671 0.15497 0.0765
Scidocs sqlite 17.85 670 56.64 0.15156 0.08822 0.15717 0.0776
Scidocs 等级 13.11 1056 162.99 0.14932 0.08670 0.15408 0.0761
df[df.source == "scifact"].reset_index(drop=True)
来源 方法 索引 记忆 搜索 ndcg_cut_10 map_cut_10 recome_10 p_10
scifact BM25 5.51 653 1.07 0.66324 0.61764 0.78761 0.08700
scifact ES 2.90 625 9.62 0.66058 0.61518 0.78428 0.08667
scifact sqlite 1.85 631 20.28 0.66630 0.61966 0.79494 0.08800
scifact 等级 1.85 724 42.22 0.65618 0.61204 0.77400 0.08500

TXTAI的实现再次与Elasticsearch进行了很好的比较。精度指标各不相同,但都相同。

重要的是要注意,在使用固态存储的内部测试中,Elasticsearch和Txtai的速度大致相同。这些时代的Elasticsearch有点慢是在Google Colab环境中运行的产物。

包起来

本笔记本显示了如何在Python中构建有效的稀疏关键字索引。基准测试表明,从准确性和速度的角度,TXTAI与Apache Lucene相当地提供了强大的实现。

此关键字索引可以用作python中的独立索引,也可以与密度向量索引组合形成hybrid索引。