DEV Community

Cover image for 🧠 How We Upgraded Our WordPress Search with OpenSearch Neural + Cohere for Multilingual Semantic Search
Jayesh Shinde
Jayesh Shinde

Posted on

🧠 How We Upgraded Our WordPress Search with OpenSearch Neural + Cohere for Multilingual Semantic Search

At our company, we use WordPress as a knowledge base (KB) for internal articles.

We indexed those articles in OpenSearch, but the default keyword search felt… old-school.

  • Searching “Netflix subscription” missed “How to manage ネットフリックス plans”.
  • Searching “AWS cost optimization” returned random hits because keywords didn’t align.

So we upgraded our search to semantic search using the OpenSearch ML plugin + Cohere embeddings served via Amazon Bedrock — a great combination for multilingual understanding and secure enterprise integration.


⚙️ The Problem

Our initial setup used simple keyword mapping:

{
  "properties": {
    "title": { "type": "text" },
    "content": { "type": "text" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Even after tweaking analyzers, users searching in Japanese (katakana) or English weren’t getting expected matches.

For example, “netflix” and “ネットフリックス” should be the same — but OpenSearch treated them as completely different tokens.


🚀 The Plan

We wanted to add semantic search on top of our existing index.
That means converting both documents and queries into vectors using the same embedding model, and comparing them using cosine similarity.

Our plan:

  1. Create an ML connector for the Cohere embedding API
  2. Create a model for document embeddings (input_type = search_document)
  3. Create another model for query embeddings (input_type = search_query)
  4. Update our ingestion pipeline to generate vectors during indexing
  5. Use script_score (or kNN query) to retrieve the best semantic matches

🔌 Step 1: Create a Connector (to Cohere)

First, we create an ML connector in OpenSearch to call Cohere’s API.

POST _plugins/_ml/connectors/_create
{
  "name": "bedrock-cohere-doc-connector",
  "description": "Amazon Bedrock connector for Cohere embeddings (document)",
  "protocol": "aws_sigv4",
  "parameters": {
    "region": "us-east-1",
    "service_name": "bedrock",
    "model_id": "cohere.embed-multilingual-v3",
    "input_type": "search_document"
  }
}
Enter fullscreen mode Exit fullscreen mode

Here:

protocol: aws_sigv4 makes OpenSearch sign Bedrock API calls with IAM credentials.

model_id refers to Cohere’s multilingual embedding model hosted on Bedrock.

We use input_type=search_document for document-level embeddings.


🧩 Step 2: Register the Model

Now, we create a model using the connector:

POST _plugins/_ml/models/_register
{
  "name": "bedrock-cohere-doc-embed",
  "function_name": "embedding",
  "connector_id": "bedrock-cohere-doc-connector"
}

POST _plugins/_ml/models/bedrock-cohere-doc-embed/_deploy
Enter fullscreen mode Exit fullscreen mode

This model will be used in our ingestion pipeline.

Then deploy it:

POST _plugins/_ml/models/cohere-doc-embed-model/_deploy
Enter fullscreen mode Exit fullscreen mode

OpenSearch now knows how to call Cohere to embed documents.


💾 Step 3: Create the Index with a Vector Field

Next, we create our knowledge base index that includes a vector field for embeddings.

PUT kb-articles
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { "type": "text" },
      "embedding": {
        "type": "knn_vector",
        "dimension": 1024,
        "method": {
          "name": "hnsw",
          "space_type": "cosinesimil",
          "engine": "nmslib",
          "parameters": {
            "ef_construction": 512,
            "m": 16
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The vector field embedding will hold our document-level embeddings from Cohere (1024 dimensions).

🧠 Step 4: Add an Ingest Pipeline

We’ll generate document embeddings automatically during ingestion.

PUT _ingest/pipeline/kb-embed-pipeline
{
  "processors": [
    {
      "ml_inference": {
        "model_id": "bedrock-cohere-doc-embed",
        "input_map": { "title": "text" },
        "output_map": { "embedding": "embedding" }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now, when we index a document:

POST kb-articles/_doc?pipeline=kb-embed-pipeline
{
  "title": "Netflix subscription help",
  "content": "Steps to manage your Netflix account and billing."
}
Enter fullscreen mode Exit fullscreen mode

OpenSearch automatically calls Amazon Bedrock, retrieves Cohere’s embedding, and stores it in the embedding vector field.


💬 Step 5: Handle User Queries with Another Connector

Initially, I used the same connector (with input_type=search_document) for both documents and queries.
That caused a mismatch — “ネットフリックス” (Katakana) and “Netflix” were still not matching.

The fix was to create another connector and model specifically for query embeddings.

POST _plugins/_ml/connectors/_create
{
  "name": "bedrock-cohere-query-connector",
  "description": "Amazon Bedrock connector for Cohere embeddings (query)",
  "protocol": "aws_sigv4",
  "parameters": {
    "region": "us-east-1",
    "service_name": "bedrock",
    "model_id": "cohere.embed-multilingual-v3",
    "input_type": "search_query"
  }
}
Enter fullscreen mode Exit fullscreen mode

and then register and deploy another model:

POST _plugins/_ml/models/_register
{
  "name": "bedrock-cohere-query-embed",
  "function_name": "embedding",
  "connector_id": "bedrock-cohere-query-connector"
}

POST _plugins/_ml/models/bedrock-cohere-query-embed/_deploy
Enter fullscreen mode Exit fullscreen mode

This ensures both documents and queries are embedded in compatible vector spaces.

Now, when we run a search, we first generate a query embedding via the ML model’s /predict endpoint:

POST _plugins/_ml/models/bedrock-cohere-query-embed/_predict
{
  "text": "ネットフリックス subscription"
}
Enter fullscreen mode Exit fullscreen mode

🔍 Step 6: Semantic Search Query

Finally, we plug the embedding into a script_score query to rank results by cosine similarity:

POST kb-articles/_search
{
  "size": 5,
  "query": {
    "script_score": {
      "query": { "match_all": {} },
      "script": {
        "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
        "params": {
          "query_vector": [/* embedding array from _predict */]
        }
      }
    }
  },
  "sort": [
    { "_score": { "order": "desc" } }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now “Netflix” and “ネットフリックス” both match beautifully.
🎯 Cohere’s multilingual embeddings + OpenSearch vector search did the trick.


🧩 Why We Chose Cohere

  • Strong multilingual understanding — perfect for English + Japanese content
  • Easy integration — just plug in an API key via OpenSearch ML connector
  • Consistent embedding dimensions — works well with knn_vector
  • Fast inference — good for production-scale pipelines

🪛 Troubleshooting Tips

Problem Cause Fix
Katakana & English not matching Used search_document for query embeddings Create a separate connector with input_type=search_query
“Dimension mismatch” errors Wrong embedding model or field dimension Make sure both model & field use same dimension
Inference timeout Cohere API rate limits Batch or cache embeddings during ingestion
Weird scores Missing space_type: cosinesimil Use cosine similarity in mapping

✅ Summary

We started with basic keyword search and ended up with multilingual semantic search using:

  • OpenSearch ML plugin
  • Cohere embedding models
  • Cosine similarity on knn_vector
  • Separate connectors for documents vs. queries

Now users can search KB articles naturally — whether they type “Netflix” or “ネットフリックス”.
Same meaning, same results.


💡 If you’re building multilingual or brand-sensitive search, don’t skip the input_type difference — it can make or break your semantic matching.


Top comments (0)