Wikipedia 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.

using SimilaritySearch
using TextSearch
using JSON3
using Markdown
using Printf

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"))
end
search_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") |> display

Query: «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") |> display

Query: «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") |> display

Query: «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") |> display

Query: «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)) |> display

With 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`