Build a Document Search App in About 10 Lines of Python
Sometimes you just want to search your own documents from Python.
For example:
- Search notes or text files by keyword
- Search JSON data
- Filter documents by category
- Combine keyword search with metadata
- Add search to a small local application
You could set up a dedicated search server, but that may be more infrastructure than you need.
With nlp4j-local-search, you can create a small local document search engine directly from Python.
Let's start with the code.
Installation
pip install nlp4j-local-search==0.7.4.0
Latest package:
https://pypi.org/project/nlp4j-local-search/0.7.4.0/
A Document Search App in About 10 Lines
from nlp4j_local_search import SearchEngine
with SearchEngine("en") as engine:
engine.add("1", "Kyoto is a historic city in Japan.", fields={"category": "city"})
engine.add("2", "Nintendo is headquartered in Kyoto.", fields={"category": "company"})
engine.add("3", "Tokyo is the capital city of Japan.", fields={"category": "city"})
engine.commit()
for r in engine.search("Kyoto"):
print(r.id, r.body)
That's basically it.
The result will look like:
1 Kyoto is a historic city in Japan.
2 Nintendo is headquartered in Kyoto.
No separate search server is required.
How It Works
The basic workflow is very small:
add documents
↓
commit
↓
search
First, create a search engine.
with SearchEngine("en") as engine:
Then add some documents.
engine.add("1", "Kyoto is a historic city in Japan.")
The first argument is the document ID.
The second argument is the text to search.
After adding documents, commit them to the search index.
engine.commit()
Then search.
engine.search("Kyoto")
For a simple document search application, this is almost everything you need.
Add Metadata to Documents
Real-world documents usually have more than just text.
For example, a document may have:
text
category
country
author
product
year
You can attach additional fields when adding a document.
engine.add(
"1",
"Kyoto is a historic city in Japan.",
fields={"category": "city"},
)
Now the document has both text and metadata.
Conceptually:
id = 1
text = Kyoto is a historic city in Japan.
category = city
Search by Category
You can search metadata fields too.
engine.search("category:city")
This finds documents whose category is city.
For example:
for r in engine.search("category:city"):
print(r.body)
could return:
Kyoto is a historic city in Japan.
Tokyo is the capital city of Japan.
Combine Keyword Search and Metadata
This becomes more useful when you combine text search with metadata.
engine.search("Kyoto AND category:company")
This means:
contains "Kyoto"
AND
category is "company"
In our example, the result is:
Nintendo is headquartered in Kyoto.
The Python code is simply:
for r in engine.search("Kyoto AND category:company"):
print(r.body)
OR Search
You can also search for multiple alternatives.
engine.search("Kyoto OR Tokyo")
This finds documents containing either Kyoto or Tokyo.
for r in engine.search("Kyoto OR Tokyo"):
print(r.body)
JSON Documents
You can also add documents as dictionaries.
engine.add_json({
"id": "1",
"body": "Kyoto is a historic city in Japan.",
"category": "city",
"country": "Japan",
})
This is convenient when your source data already comes from JSON or JSONL files.
You can then search fields directly.
engine.search("category:city")
or combine multiple conditions:
engine.search("category:city AND country:Japan")
That makes it possible to build a small structured document search application without introducing a database-style query layer.
Search Results Include Scores
Search results also include a relevance score.
for r in engine.search("Kyoto"):
print(r.id, r.body, r.score)
A result may look like:
1 Kyoto is a historic city in Japan. 0.18...
2 Nintendo is headquartered in Kyoto. 0.16...
The score can be useful when you want to rank search results by relevance.
More Search Expressions
Once you need more advanced queries, you can extend the search expression.
AND:
engine.search("Kyoto AND historic")
OR:
engine.search("Kyoto OR Tokyo")
NOT:
engine.search("category:city AND NOT country:Japan")
Field search:
engine.search("category:company")
Keyword + field:
engine.search("Kyoto AND category:company")
Wildcard:
engine.search("Kyo*")
Phrase search:
engine.search('"historic city"')
You can start with:
search("Kyoto")
and learn the more advanced forms only when you actually need them.
What's Running Under the Hood?
So far, we have not needed to know anything about the search engine implementation.
Internally, nlp4j-local-search uses Apache Lucene.
Lucene is a mature full-text search library used as the foundation of many search systems.
The architecture is roughly:
Python application
↓
nlp4j-local-search
↓
Apache Lucene
The idea is that Python users do not need to work directly with Lucene's Java APIs just to add local search to a program.
You can start from a Python API such as:
engine.add(...)
engine.commit()
engine.search(...)
and use the underlying search engine without thinking much about the Java layer.
Why Not Just Use Elasticsearch or OpenSearch?
Elasticsearch, OpenSearch, and Solr are excellent choices when you need a full search server.
For example:
- Distributed search
- Multiple application servers
- Large-scale production deployments
- REST APIs shared by many clients
- Cluster management
But sometimes the requirement is much smaller.
You may simply want to:
- Search a local JSONL dataset
- Explore documents in Jupyter
- Build a prototype
- Add search to a command-line tool
- Search a few thousand or a few hundred thousand documents
- Prepare retrieval for an NLP or RAG experiment
In those cases, setting up a separate search server may feel unnecessary.
nlp4j-local-search runs the search engine inside your Python application.
Vector Search Is Also Available
The same package can also perform vector search.
For example:
with SearchEngine("en", vector_dimension=2) as engine:
engine.add("1", [1.0, 0.0])
engine.add("2", [0.0, 1.0])
engine.commit()
results = engine.search_vector([0.9, 0.1])
So the same local search engine can support:
keyword search
+
field filtering
+
vector search
You do not need vector search to get started, though.
For many applications, plain text search is still the simplest and most useful first step.
From Search to Data Exploration
You can also inspect the distribution of field values.
For example:
print(engine.view("category"))
This can show which categories appear most often in your documents.
That means the package can be used not only for retrieving individual documents, but also for exploring the contents of a dataset.
Summary
A basic local document search application can be surprisingly small.
from nlp4j_local_search import SearchEngine
with SearchEngine("en") as engine:
engine.add("1", "Kyoto is a historic city in Japan.")
engine.add("2", "Nintendo is headquartered in Kyoto.")
engine.commit()
for r in engine.search("Kyoto"):
print(r.body)
Start with:
search("Kyoto")
Then, when necessary, add metadata:
search("category:city")
Combine text and metadata:
search("Kyoto AND category:company")
And later, if your application needs it, move on to aggregation or vector search.
The goal of nlp4j-local-search is simple:
make local document search feel like a normal Python operation.