A lightweight local document ingestion and retrieval package.
Project description
searchbox
searchbox is a lightweight Python package for modular search systems.
It does one thing:
- read local files or folders
- extract text from supported document types
- split long text into chunks
- store chunks through a pluggable repository layer
- fetch candidates through a pluggable source layer
- rank candidates through pluggable search strategies
Install
Install from the current project directory:
pip install .
If you later publish it to PyPI under the same package name:
pip install searchbox
What It Supports
Supported file types:
.txt,.md,.rst.py,.toml,.yaml,.yml.csv,.tsv.json.html,.htm.pdf
Default document storage backend:
- SQLite database file on local disk via
SqliteDocumentRepository
Default document retrieval behavior:
- documents are stored as chunks internally
- candidates are fetched from a source abstraction
- search is performed on chunks with BM25 ranking by default
- results are returned as document-level hits
- each result includes the best matching chunk text and matched chunk indexes
Architecture:
BaseSearchdefines the shared search workflowSearchStrategydefines ranking modes such as BM25, vector, semantic, or generative searchCandidateSourcedefines how candidates are fetchedDocumentRepositorydefines how chunks are persistedDocumentIngestorhandles file ingestion independently from search- this makes it easier to replace local SQLite with another database later
Quick Start
from searchbox import docsearch
engine = docsearch(db_path="docs.db", chunk_size=240, chunk_overlap=40)
engine.add_path("docs")
results = engine.search("how does indexing work", top_k=3)
for item in results:
print(item["id"], item["score"], item["metadata"]["matched_chunks"])
Main API
Create a search engine:
from searchbox import docsearch
engine = docsearch(
db_path="docs.db",
chunk_size=240,
chunk_overlap=40,
)
Available methods:
docsearch(db_path="searchbox.db", chunk_size=240, chunk_overlap=40)add_path(path, recursive=True, include_hidden=False, chunk_size=None, chunk_overlap=None)add_paths(paths, recursive=True, include_hidden=False, chunk_size=None, chunk_overlap=None)search(query, top_k=5)search_pretty(query, top_k=5, snippet_length=280)response(query, top_k=5)count()clear()supported_extensions()
Advanced composition:
docsearch(..., repository=..., source=..., strategies=..., extractor=...)websearch(web_fetcher, strategies=...)SqliteDocumentRepositoryRepositoryCandidateSourceBM25SearchStrategyBaseSearchSearchFactoryFileTextExtractor
Constructor Parameters
docsearch(...)
engine = docsearch(
db_path="searchbox.db",
chunk_size=240,
chunk_overlap=40,
)
Parameters:
db_path: SQLite database path; default issearchbox.dbchunk_size: number of words per chunk; default is240chunk_overlap: overlapping words between neighboring chunks; default is40repository: optional custom repository implementationsource: optional custom candidate source implementationstrategies: optional mapping of search strategiesextractor: optional custom file text extractordefault_mode: which strategy name to use by default
Notes:
chunk_sizemust be greater than0chunk_overlapmust be greater than or equal to0chunk_overlapmust be smaller thanchunk_size
Ingestion Methods
add_path(path, recursive=True, include_hidden=False, chunk_size=None, chunk_overlap=None)
Index one file or one directory.
If path is:
- a file: only that file is indexed
- a directory: files inside the directory are indexed
Returns:
- a list of inserted chunk ids
Parameters:
path: file path or directory pathrecursive: whenpathis a directory, whether to scan subdirectories; defaultTrueinclude_hidden: whether to include hidden files and hidden directories; defaultFalsechunk_size: optional per-call override of constructorchunk_sizechunk_overlap: optional per-call override of constructorchunk_overlap
Example:
engine.add_path("docs")
engine.add_path("README.md")
engine.add_path("docs", recursive=False)
add_paths(paths, recursive=True, include_hidden=False, chunk_size=None, chunk_overlap=None)
Index multiple files or directories in one call.
Parameters:
paths: a sequence of file or directory paths- all other parameters behave the same as
add_path(...)
Example:
engine.add_paths(
[
"docs",
"notes/project.md",
"data/reference",
]
)
Search Methods
search(query, top_k=5)
Search the indexed documents and return the most relevant document hits.
Parameters:
query: search texttop_k: maximum number of returned documents; default5
Returns:
- a list of result dictionaries sorted by descending relevance
Current default ranking:
- BM25 over chunk text plus title text
- document results are produced by aggregating chunk-level hits
You can also pass another strategy mode:
results = engine.search("what is rag", mode="bm25")
response(query, top_k=5)
Alias of search(query, top_k=5).
Use whichever name you prefer.
search_pretty(query, top_k=5, snippet_length=280)
Search the indexed documents and return a more display-friendly result shape.
Parameters:
query: search texttop_k: maximum number of returned documents; default5snippet_length: max snippet length in characters; default280
Returns:
- a list of simplified dictionaries with
path,file_name,score,matched_chunk_count,matched_chunks,best_chunk_index, andsnippet
Examples
Index a single file
from searchbox import docsearch
engine = docsearch(db_path="single.db")
engine.add_path("guide.txt")
results = engine.search("installation guide", top_k=1)
print(results)
Index a directory
from searchbox import docsearch
engine = docsearch(db_path="docs.db")
engine.add_path("docs")
results = engine.search("local retrieval", top_k=3)
for item in results:
print(item["id"], item["title"], item["score"])
Index multiple locations
from searchbox import docsearch
engine = docsearch(db_path="multi.db")
engine.add_paths(["docs", "notes", "README.md"])
results = engine.search("api design", top_k=5)
for item in results:
print(item["metadata"]["source_path"])
Override chunk settings for one ingestion call
from searchbox import docsearch
engine = docsearch(db_path="docs.db", chunk_size=240, chunk_overlap=40)
engine.add_path(
"manuals",
chunk_size=120,
chunk_overlap=20,
)
Show supported file types
from searchbox import docsearch
engine = docsearch()
print(engine.supported_extensions())
Use your own repository, source, or strategies
from searchbox import BM25SearchStrategy, DocumentSearch, RepositoryCandidateSource, SqliteDocumentRepository
repository = SqliteDocumentRepository(db_path="docs.db")
source = RepositoryCandidateSource(repository)
strategies = {
"bm25": BM25SearchStrategy(),
}
engine = DocumentSearch(
repository=repository,
source=source,
strategies=strategies,
chunk_size=240,
chunk_overlap=40,
)
Get display-friendly search output
from searchbox import docsearch
engine = docsearch(db_path="docs.db")
engine.add_path("papers")
results = engine.search_pretty("vllm", top_k=3)
for item in results:
print(item["path"])
print(item["score"])
print(item["matched_chunk_count"])
print(item["snippet"])
print("---")
Search Result Format
Each search result is a document-level hit.
Example:
[
{
"id": "guide.pdf",
"title": "guide.pdf",
"content": "best matching chunk text ...",
"score": 0.82,
"metadata": {
"source_path": "guide.pdf",
"source_name": "guide.pdf",
"chunk_count": 4,
"matched_chunks": [0, 1],
"best_chunk_index": 1,
},
}
]
Field meaning:
id: document identifier; currently the source file path string used by the result layertitle: display title derived from the file namecontent: the best matching chunk text for that documentscore: relevance scoremetadata["source_path"]: file path stored for the source documentmetadata["source_name"]: file name onlymetadata["chunk_count"]: total number of stored chunks for the filemetadata["matched_chunks"]: chunk indexes that matched the querymetadata["best_chunk_index"]: chunk index whose text is returned incontent
How Ingestion Works
When you call add_path(...) or add_paths(...):
searchboxreads each supported file- extracted text is normalized
- long text is split into overlapping word chunks
- every chunk is stored through the configured repository
- later, a candidate source fetches chunks for search
- the active search strategy scores candidates
- chunk hits are merged back into document-level results
This means:
- large files can still be searched effectively
- results are returned as documents, not raw chunks
contentin the result is the best matching chunk, not always the full original file
PDF Notes
PDF ingestion is supported.
Current behavior:
searchboxfirst tries the system commandpdftotext- if that fails, it uses a lightweight fallback extractor
Recommendation:
- if you care about PDF extraction quality, make sure
pdftotextis installed on the machine
Modular Design
searchbox is structured so ingestion, storage, and retrieval are decoupled:
BaseSearch: shared search flowDocSearch: document-specific search based on the shared flowWebSearch: web-specific search based on the shared flowDocumentSearch: high-level document search facadeFileTextExtractor: file reading and text extractionCandidateSource: candidate fetching interfaceRepositoryCandidateSource: candidate source backed by a repositoryDocumentRepository: storage interfaceSqliteDocumentRepository: current local SQLite implementationSearchStrategy: ranking interfaceBM25SearchStrategy: current default strategyVectorSearchStrategy,SemanticSearchStrategy,GenerativeSearchStrategy,DiffusionRetrievalStrategy: extension points for more advanced retrieval modes
This means that if you later move from local SQLite to a web or cloud database, you can add a new repository implementation and keep the ingestion and retrieval API stable.
Utility Methods
count()
Return the number of stored chunk rows in the SQLite database.
Example:
count = engine.count()
print(count)
clear()
Delete all stored chunk rows from the database.
Example:
engine.clear()
supported_extensions()
Return the currently supported file extensions.
Example:
print(engine.supported_extensions())
Behavior and Edge Cases
- unsupported file types are skipped
- empty or unreadable extracted content is skipped
- hidden files are skipped by default when scanning directories
- if a given path does not exist,
FileNotFoundErroris raised - if
queryis empty or blank,ValueErroris raised - if no document matches,
search(...)returns an empty list
Minimal End-to-End Example
from searchbox import docsearch
engine = docsearch(db_path="example.db")
engine.add_paths(
[
"docs",
"README.md",
"notes/plan.json",
]
)
results = engine.search("sqlite document retrieval", top_k=3)
for item in results:
print("path:", item["metadata"]["source_path"])
print("score:", item["score"])
print("best chunk:", item["content"])
print("matched chunks:", item["metadata"]["matched_chunks"])
print("---")
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file searchbox-0.3.0.tar.gz.
File metadata
- Download URL: searchbox-0.3.0.tar.gz
- Upload date:
- Size: 17.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad2019763d0ed25b1318ac8353a6dcc3d941b7a42b77f67ccd3178a699e7fc88
|
|
| MD5 |
eb2ac56f5ee60c680e024bbfc815882f
|
|
| BLAKE2b-256 |
246c6b3edb2d6f7286437bb67d8d6c23b5c8316865b8126bdc0f7cd64325dcfc
|
File details
Details for the file searchbox-0.3.0-py3-none-any.whl.
File metadata
- Download URL: searchbox-0.3.0-py3-none-any.whl
- Upload date:
- Size: 16.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be7749e6300ad55f9cc81f79aae9c6947ad32e2166cbc1865792764cf9c924b4
|
|
| MD5 |
128418c07114545469964b5aec4ae8c2
|
|
| BLAKE2b-256 |
1cb51ecd99caa6ca0017ed852792aef94844864791b0188c071d9335effcf6c2
|