using SimilaritySearch
using TextSearch
using JSON3
using Markdown
using PrintfWikipedia Full-Text Search with BM25 and Inverted Files
by: Eric S. Téllez
This demonstration illustrates high-performance full-text search using BM25 Inverted Files with TextSearch.jl and SimilaritySearch.jl.
Unlike a conventional inverted index that computes term statistics locally on the loaded documents, BM25InvertedFile can leverage a pre-trained TextProfile (trained over 14.5 million paragraphs of Spanish Wikipedia) to borrow corpus-wide Inverse Document Frequencies (IDF) and average document lengths.
Loading the Wikipedia Profile and Corpus
We load the pre-trained Spanish Wikipedia profile and a sample collection of 60,000 paragraphs from the JSONL corpus.
# 1. Load the pre-trained profile containing global vocabulary and IDF statistics
profile_path = joinpath(@__DIR__, "..", "..", "TextSearch.jl", "corpus-profiles", "profiles", "wiki20231101-es-paragraphs.zip")
profile = load_profile(profile_path)
@info "Profile loaded:" vocsize=vocsize(profile.model.voc) trainsize=profile.model.voc.trainsize[]
# 2. Load paragraphs from the JSONL corpus
corpus_path = joinpath(@__DIR__, "..", "..", "TextSearch.jl", "corpus-profiles", "work", "wikipedia", "wiki20231101-es-paragraphs.jsonl")
const MAX_DOCS = 60_000
texts = String[]
titles = String[]
urls = String[]
open(corpus_path) do f
for (i, line) in enumerate(eachline(f))
i > MAX_DOCS && break
isempty(strip(line)) && continue
obj = JSON3.read(line)
push!(texts, String(obj.text))
push!(titles, String(obj.title))
push!(urls, String(obj.url))
end
end
@info "Loaded paragraphs: $(length(texts))"Building the BM25 Index
We construct the BM25InvertedFile using the loaded profile. We enable query expansion (expansion=true) to widen queries with the profile’s semantic expansion network.
idx = BM25InvertedFile(profile; expansion=true)
ctx = InvertedFileContext()
# Parallel in-memory indexing
@time append_items!(idx, ctx, texts)
@info "Indexed documents in BM25: $(length(idx))"Search and Display Helpers
We define a helper function to execute queries and format the retrieved documents:
function search_wikipedia(invfile, ctx, qtext::String, k::Int=5; t::Int=1, policy::QueryPolicy=invfile.query.policy)
res = knnqueue(KnnSorted, k)
t_search = @elapsed search(invfile, ctx, qtext, res; t, policy)
md = [
"### Query: _«$qtext»_",
"Search time: **$(@sprintf "%.2f ms" (t_search * 1000))**",
"",
"| # | Article Title | BM25 Score | Snippet |",
"|---|---------------|------------|---------|"
]
for (j, p) in enumerate(IdDistView(res))
doc_id = p.id
score = -p.dist # BM25 stores negated scores internally
tit = titles[doc_id]
url = urls[doc_id]
txt = replace(texts[doc_id], "\n" => " ")
snippet = length(txt) > 140 ? first(txt, 140) * "..." : txt
push!(md, "| $j | [**$tit**]($url) | $(@sprintf "%.2f" score) | $snippet |")
end
Markdown.parse(join(md, "\n"))
endsearch_wikipedia (generic function with 2 methods)
Thematic Search Examples
We test several conceptual queries on Spanish Wikipedia:
search_wikipedia(idx, ctx, "agujero negro gravitacional horizonte de sucesos") |> displayQuery: «agujero negro gravitacional horizonte de sucesos»
Search time: 51.58 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | Horizonte de sucesos | 119.48 | Horizonte de sucesos En 1784, John Michell propuso que la gravedad puede ser lo suficientemente fuerte en las proximidades de objetos compac... |
| 2 | Agujero negro | 118.92 | Agujero negro Un agujero negro es una región finita del espacio descrita en las ecuaciones de Einstein, cuyo interior posee una concentraci... |
| 3 | Agujero negro | 110.92 | Agujero negro Descubrimientos recientes En 1995 un equipo de investigadores de la UCLA dirigido por Andrea Ghez demostró mediante simulació... |
| 4 | Agujero negro | 110.77 | Agujero negro Radiación de Hawking Hasta principios de 1970 se pensaba que los agujeros negros no emitían directamente ningún tipo de materi... |
| 5 | Horizonte de sucesos | 108.98 | Horizonte de sucesos Horizonte de sucesos de un agujero negro rotativo El horizonte de sucesos es una superficie imaginaria de forma esféri... |
search_wikipedia(idx, ctx, "revolución industrial máquina de vapor ferrocarril") |> displayQuery: «revolución industrial máquina de vapor ferrocarril»
Search time: 8.92 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | León (España) | 74.49 | León (España) El sector secundario leonés se caracteriza por su debilidad y por inexistencia de grandes empresas que generen un entramado em... |
| 2 | Energía eólica | 74.29 | Energía eólica Una turbina eólica es una máquina que transforma la energía del viento en energía mecánica mediante unas aspas oblicuas unida... |
| 3 | Barcelona | 73.09 | Barcelona Economía Históricamente la economía de Barcelona se ha basado en el comercio, que gracias a su enclave geográfico estratégico le h... |
| 4 | Geografía industrial | 66.14 | Geografía industrial Factores de localización industrial Recursos naturales o materias primas Fuentes de energía (inicialmente el carbón par... |
| 5 | Barcelona | 64.53 | Barcelona Revolución industrial La recuperación económica iniciada a finales del y la industrialización en el propiciaron que Barcelona vo... |
search_wikipedia(idx, ctx, "genética ADN estructura doble hélice") |> displayQuery: «genética ADN estructura doble hélice»
Search time: 7.58 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | Genética | 112.83 | Genética En 1941 Edward Lawrie Tatum y George Wells Beadle demostraron que los genes ARN mensajero codifican proteínas; luego en 1953 James ... |
| 2 | Historia de la biología | 110.04 | Historia de la biología Oswald Avery mostró en 1943 que el ADN era probablemente el material genético de los cromosomas, y no sus proteínas;... |
| 3 | Ácido ribonucleico | 100.61 | Ácido ribonucleico Genomas de ARN El ADN es la molécula portadora de la información genética en todos los organismos celulares, pero, al ig... |
| 4 | Biología celular | 98.44 | Biología celular Núcleo: El núcleo de la célula funciona como genoma y almacén de información genética de la célula, conteniendo todo el ADN... |
| 5 | Ácido ribonucleico | 97.05 | Ácido ribonucleico Hélice A A diferencia del ADN las moléculas de ARN suelen ser de cadena simple y no forman dobles hélices extensas, no ob... |
search_wikipedia(idx, ctx, "imperio romano julio césar senado") |> displayQuery: «imperio romano julio césar senado»
Search time: 10.63 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | Siglo I a. C. | 97.83 | Siglo I a. C. Acontecimientos relevantes Guerras y política República romana. 88 a. C.: Inician las Guerras mitridáticas, con las cuales l... |
| 2 | Andalucía | 96.97 | Andalucía Andalucía quedó incorporada plenamente al Imperio romano con su conquista y romanización, creándose la provincia de la Bética, sub... |
| 3 | Imperio romano | 94.12 | Imperio romano Aunque los emperadores julio-claudios alentaron el uso del latín en la realización de asuntos oficiales en todo el imperio, e... |
| 4 | España | 92.44 | España La intervención romana se produjo en la segunda guerra púnica (218 a. C.), que inició una paulatina conquista romana de Hispania, no ... |
| 5 | Siglo XV | 89.07 | Siglo XV Personajes relevantes Personalidades políticas y militares Afonso de Albuquerque (1453-1515), noble y oficial naval portugués cuyas... |
Literal Search vs. Query Expansion
The query expansion network derived from LSI enables matching documents even when the query uses synonyms or related terms not literally present in the document.
We can adjust query behavior via QueryPolicy(expansion=...):
query = "astronomía planetas celestes"
display(Markdown.parse("#### With Semantic Expansion (`expansion=true`):"))
search_wikipedia(idx, ctx, query, 4; policy=QueryPolicy(expansion=true)) |> display
display(Markdown.parse("#### Literal Search (`expansion=false`):"))
search_wikipedia(idx, ctx, query, 4; policy=QueryPolicy(expansion=false)) |> displayWith Semantic Expansion (expansion=true):
Query: «astronomía planetas celestes»
Search time: 5.44 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | Unidad astronómica | 86.97 | Unidad astronómica Posteriormente Johannes Kepler, basándose en las cuidadosas observaciones de Tycho Brahe, estableció las leyes del movimi... |
| 2 | Astronomía | 82.83 | Astronomía Galileo añadió la novedad del uso del telescopio para mejorar sus observaciones. La disponibilidad de datos observacionales preci... |
| 3 | Astronomía | 74.74 | Astronomía Astronomía de posición. Tiene por objeto situar en la esfera celeste la posición de los astros midiendo determinados ángulos resp... |
| 4 | Estrella | 73.92 | Estrella Clasificación de estrellas por sistema planetario Las estrellas que forman parte de un sistema planetario se denominan estrellas p... |
Literal Search (expansion=false):
Query: «astronomía planetas celestes»
Search time: 0.32 ms
| # | Article Title | BM25 Score | Snippet |
|---|---|---|---|
| 1 | Astronomía | 48.08 | Astronomía Mecánica celeste Astrofísica Estudio de los objetos celestes El sistema solar desde la astronomía El estudio del universo o cosmo... |
| 2 | Astronomía | 45.17 | Astronomía La astronomía (del griego άστρον [ástron] 'estrella' y νομία [nomía] 'normas', 'leyes de las estrellas') es la ciencia natural qu... |
| 3 | Astronomía | 41.89 | Astronomía Galileo añadió la novedad del uso del telescopio para mejorar sus observaciones. La disponibilidad de datos observacionales preci... |
| 4 | Astronomía | 39.49 | Astronomía Etimológicamente hablando la astronomía es la ciencia que trata de la magnitud, medida y movimiento de los cuerpos celestes. |
Search Throughput
We evaluate batch query throughput in queries per second (QPS):
test_queries = [
"inteligencia artificial", "mecánica cuántica", "arquitectura gótica",
"filosofía presocrática", "teorema de pitágoras", "cambio climático",
"sistema nervioso central", "economía de mercado", "energía nuclear",
"pintura renacentista"
]
res = knnqueue(KnnSorted, 5)
t_total = @elapsed begin
for q in test_queries
reuse!(res, 5)
search(idx, ctx, q, res)
end
end
qps = length(test_queries) / t_total
Markdown.parse("""
**Throughput:** $(@sprintf "%.1f" qps) queries per second on an index of $(length(idx)) documents.
""")Throughput: 161.2 queries per second on an index of 60000 documents.
Environment and dependencies
Julia Version 1.12.7 Commit 6d172b025e4 (2026-08-15 08:05 UTC) Build Info: Official https://julialang.org release Platform Info: OS: Linux (x86_64-linux-gnu) CPU: 64 × Intel(R) Xeon(R) Silver 4216 CPU @ 2.10GHz WORD_SIZE: 64 LLVM: libLLVM-18.1.7 (ORCJIT, cascadelake) GC: Built with stock GC Threads: 64 default, 1 interactive, 64 GC (on 64 virtual cores) Environment: JULIA_PROJECT = @. JULIA_NUM_THREADS = auto JULIA_LOAD_PATH = @:@stdlib Status `~/Research/SimilaritySearchDemos/Project.toml` [aaaa29a8] Clustering v0.15.8 [944b1d66] CodecZlib v0.7.9 ⌅ [5ae59095] Colors v0.12.11 [a93c6f00] DataFrames v1.8.2 [f67ccb44] HDF5 v0.17.3 [0f8b85d8] JSON3 v1.14.3 [23fbe1c1] Latexify v0.16.12 [eb30cadb] MLDatasets v0.7.21 [06eb3307] ManifoldLearning v0.9.0 ⌅ [ca7969ec] PlotlyLight v0.11.1 [91a5bcdd] Plots v1.41.7 [27ebfcd6] Primes v0.5.7 [92933f4c] ProgressMeter v1.11.0 [ca7ab67e] SimSearchManifoldLearning v0.4.0 `~/Research/SimSearchManifoldLearning.jl` [053f045d] SimilaritySearch v1.2.0 `~/Research/SimilaritySearch.jl` ⌅ [2913bbd2] StatsBase v0.33.21 [7f6f6c8a] TextSearch v1.1.1 `~/Research/TextSearch.jl` Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated`