0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Super Easy Vector Search in Python with nlp4j-local-search 0.8.0.0

0
Posted at

Super Easy Vector Search in Python with nlp4j-local-search 0.8.0.0

Vector search sounds simple:

Convert text into vectors and find similar vectors.

But when you actually try to build it, there is often quite a lot to prepare.

You need to:

  • choose an embedding model
  • load the model
  • convert documents into vectors
  • store the vectors
  • set up a vector search engine or vector database
  • convert search queries into vectors
  • perform similarity searches

That is a lot of preparation if you just want to experiment with vector search.

With nlp4j-local-search 0.8.0.0, I wanted to make this much simpler.

The goal is:

Vector search should feel like ordinary Python search.

PyPI:

Installation

First, install nlp4j-local-search.

pip install nlp4j-local-search

For automatic text embedding, also install the embedding package.

pip install nlp4j-local-search-embedding

By default, the embedding package uses:

intfloat/multilingual-e5-large

This is a 1024-dimensional multilingual embedding model.

The model is downloaded from Hugging Face on the first run, so the initial execution may take some time. The model is approximately 2.2 GB.

Let's Try Vector Search

Here is some very simple English data.

from nlp4j_local_search import SearchEngine

documents = [
    {"id": "1", "body": "Kyoto was the ancient capital of Japan."},
    {"id": "2", "body": "Tokyo is the largest city in Japan."},
    {"id": "3", "body": "Nara has many historic temples and shrines."},
    {"id": "4", "body": "Osaka is famous for its food culture."},
    {"id": "5", "body": "Hokkaido is known for its vast natural landscapes."},
]

Now create a search engine with:

SearchEngine("en", embedding=True)

Then add the documents and search them.

from nlp4j_local_search import SearchEngine

documents = [
    {"id": "1", "body": "Kyoto was the ancient capital of Japan."},
    {"id": "2", "body": "Tokyo is the largest city in Japan."},
    {"id": "3", "body": "Nara has many historic temples and shrines."},
    {"id": "4", "body": "Osaka is famous for its food culture."},
    {"id": "5", "body": "Hokkaido is known for its vast natural landscapes."},
]

with SearchEngine("en", embedding=True) as engine:

    for doc in documents:
        engine.add(doc["id"], doc["body"])

    engine.commit()

    for r in engine.search_semantic("old Japanese capital", limit=3):
        print(r.id, r.body, r.score)

That's basically it.

What Does embedding=True Do?

This is the important part:

SearchEngine("en", embedding=True)

When embedding=True is specified, nlp4j-local-search automatically handles the embedding process.

For example:

engine.add(
    "1",
    "Kyoto was the ancient capital of Japan."
)

internally performs the necessary text embedding and stores the resulting vector together with the searchable document.

You do not have to manually write code like:

vector = model.encode(text)

and you do not have to manually manage vectors such as:

[0.0234, -0.0182, 0.0421, ...]

The default embedding model generates 1024-dimensional vectors, but that detail can remain hidden when all you want is semantic text search.

Searching Is Just as Simple

The query is also automatically converted into an embedding.

engine.search_semantic("old Japanese capital")

So the flow is essentially:

Document text
    ↓
Automatic embedding
    ↓
Lucene vector index

Query text
    ↓
Automatic embedding
    ↓
Vector search

You provide text.

The library handles the vectors.

Search by Meaning, Not Just Words

Suppose the indexed document contains:

Kyoto was the ancient capital of Japan.

We can search for:

engine.search_semantic("old Japanese capital")

The exact phrase "old Japanese capital" does not have to appear in the document.

That is the main difference from traditional keyword search.

For example, we can try several queries:

queries = [
    "old Japanese capital",
    "a very large city",
    "historic religious buildings",
]

for query in queries:
    print(f"=== Semantic search: '{query}' ===")

    for r in engine.search_semantic(query, limit=3):
        print(f"[{r.id}] {r.body} (score: {r.score:.4f})")

A query such as:

historic religious buildings

can retrieve a semantically related document such as:

Nara has many historic temples and shrines.

This is possible even though the query and document do not contain exactly the same words.

Keyword Search Still Works

Enabling embeddings does not turn the search engine into a vector-only search engine.

Traditional keyword search is still available.

for r in engine.search("Kyoto", limit=5):
    print(r.id, r.body, r.score)

So the same SearchEngine supports both:

engine.search("Kyoto")

for keyword search, and:

engine.search_semantic("old Japanese capital")

for semantic vector search.

I intentionally separated these APIs:

search()
search_semantic()

rather than adding a search mode parameter to a single method.

This makes it obvious from the Python code which type of search is being performed.

Vector Search with a Filter

Real applications often need more than:

Find the semantically closest documents from everything.

You may instead want:

First restrict the documents by some condition, then perform vector search within those documents.

search_semantic() supports filter_query for this purpose.

For example:

for r in engine.search_semantic(
    "city",
    limit=5,
    filter_query="id:1 OR id:2",
):
    print(r.id, r.body, r.score)

The vector search is restricted to documents matching:

id:1 OR id:2

The filter uses Lucene Query Syntax.

This means that traditional field filtering and semantic vector search can be combined in the same search operation.

That becomes particularly useful when working with real datasets containing fields such as:

category
date
manufacturer
product
country
document type

You can narrow down the search space using structured conditions and then rank the matching documents by semantic similarity.

Apache Lucene 10.x Under the Hood

nlp4j-local-search 0.8.0.0 uses Apache Lucene 10.x as its underlying search engine.

From Python, however, you can work with a simple API such as:

engine.add(...)
engine.search(...)
engine.search_semantic(...)

Internally, Lucene handles the search index and vector search.

The architecture is roughly:

Python
  |
  +-- nlp4j-local-search
        |
        +-- Embedding
        |     |
        |     +-- multilingual-e5-large
        |
        +-- Apache Lucene 10.x
              |
              +-- Keyword Search
              +-- Vector Search

One important consequence is that there is no separate search server to operate.

You do not need to start Elasticsearch, OpenSearch, or Solr just to perform local searches.

You also do not need a separate vector database for this use case.

Lucene runs locally behind the Python API.

Vector Search Without Setting Up a Vector Database

This was one of the main ideas behind this feature.

When somebody says:

I just want to try vector search in Python.

it should not necessarily mean:

Choose a vector database
        ↓
Install it
        ↓
Configure it
        ↓
Start a server
        ↓
Configure a client
        ↓
Choose an embedding library
        ↓
Load a model
        ↓
Generate vectors
        ↓
Create an index
        ↓
Finally search

With nlp4j-local-search, the starting point can instead be:

pip install nlp4j-local-search
pip install nlp4j-local-search-embedding

and then:

with SearchEngine("en", embedding=True) as engine:

From there, it is just Python.

You Can Also Work with Vectors Directly

embedding=True is intended to make text-based semantic search easy.

But nlp4j-local-search 0.8.0.0 can also work with vector fields directly.

So both of these approaches are possible.

Automatic embedding:

Text
  ↓
nlp4j-local-search embedding
  ↓
Vector
  ↓
Lucene Vector Search

Or vectors provided by your application:

Your own vector
  ↓
Lucene Vector Search

This is useful if you already have vectorized data or want to use your own embedding model.

The automatic embedding feature is therefore a convenience layer rather than a requirement for using Lucene vector search.

The Minimal Example

If you remove everything that is not essential, the basic idea becomes very small:

from nlp4j_local_search import SearchEngine

with SearchEngine("en", embedding=True) as engine:

    engine.add("1", "Kyoto was the ancient capital of Japan.")
    engine.add("2", "Tokyo is the largest city in Japan.")
    engine.add("3", "Nara has many historic temples and shrines.")

    engine.commit()

    for r in engine.search_semantic("old Japanese capital"):
        print(r.body)

There is no explicit vector in this code.

There is no vector database configuration.

There is no search server configuration.

There is not even explicit embedding code.

But this is performing vector search.

Summary

With nlp4j-local-search 0.8.0.0, the essential API for semantic search is basically:

SearchEngine("en", embedding=True)

engine.add(id, text)

engine.search_semantic(query)

The library handles:

  • loading the embedding model
  • generating document embeddings
  • generating query embeddings
  • indexing vectors
  • Lucene vector search

At the same time, normal keyword search remains available through:

engine.search(query)

and semantic search can be combined with Lucene filters using:

engine.search_semantic(
    query,
    filter_query="..."
)

The idea is simple:

Make vector search feel like ordinary Python search.

If you want to experiment with semantic search without first building an entire vector-search infrastructure, nlp4j-local-search provides a very small starting point.

Links

PyPI:

GitHub:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?