Standard semantic search finds documents that are about the right topic, but it has no concept of tone. If someone searches “positive reviews about battery life,” a vanilla embedding search returns every mention of battery life regardless of whether the reviewer loved it or hated it. You can fix this by pairing FAISS-based similarity search with VADER sentiment scores, giving you a pipeline that filters on both meaning and mood.
Here’s the quick setup so you can see where this is heading:
| |
Encoding Documents and Building the FAISS Index
The first step is turning your documents into dense vectors with a sentence-transformer model and loading them into a FAISS index for fast nearest-neighbor lookup. We’ll use all-MiniLM-L6-v2 because it’s small, fast, and produces solid 384-dimensional embeddings.
| |
IndexFlatIP computes inner product, which equals cosine similarity when vectors are L2-normalized (the normalize_embeddings=True flag handles that). For datasets under a few hundred thousand documents, flat search is fast enough. Scale beyond that and you’d swap in IndexIVFFlat or IndexHNSWFlat.
Computing Sentiment Scores with VADER
VADER (Valence Aware Dictionary and sEntiment Reasoner) works well on short, informal text like reviews and social media posts. It returns a compound score between -1 (most negative) and +1 (most positive), plus breakdowns for positive, negative, and neutral proportions.
| |
Running this on the sample documents gives you scores like +0.8 for the glowing battery review and -0.7 for the one about overheating. These scores become your sentiment filter layer.
One thing to know: VADER handles negation, capitalization, and punctuation-based emphasis out of the box. “The battery is NOT good” scores negative, and “AMAZING battery!!!” scores higher positive than “amazing battery.” That’s why it’s a good fit for review data without needing a fine-tuned model.
Combining Similarity and Sentiment into a Query Interface
Now the interesting part: merging both signals. The approach is straightforward. Run the FAISS search to get the top-K semantically similar documents, then filter or re-rank based on sentiment.
| |
The boost_weight parameter controls how much sentiment strength influences ranking. A value of 0.3 means a document with a perfect +1.0 sentiment score gets a 0.3 bump on top of its cosine similarity. Set it to 0 if you only want hard filtering without re-ranking.
Let’s test it:
| |
The positive battery query surfaces the “easily lasts two full days” and “pleasant surprise” reviews. The negative query picks up the “dies before lunch” and “drains in three hours” ones. Both ignore the high-similarity but wrong-sentiment matches.
Tuning the Pipeline
A few knobs you’ll want to adjust depending on your data:
Sentiment threshold (sentiment_threshold): 0.2 works as a reasonable default for VADER. Lower it to 0.05 if you want to include mildly positive/negative text. Raise it to 0.5 if you only want strongly opinionated documents.
Boost weight (boost_weight): Start at 0.3 and evaluate results manually. If highly relevant but mildly positive documents keep getting outranked by less relevant but very positive ones, reduce the weight. If sentiment-aligned results aren’t surfacing high enough, increase it.
Candidate pool size (fetch_k): The function fetches top_k * 3 candidates before filtering. For datasets where most documents have neutral sentiment, bump this to top_k * 5 or even top_k * 10 to avoid empty results after filtering.
You can also swap VADER for a transformer-based sentiment model if your text is more formal or domain-specific. VADER excels at informal English but struggles with sarcasm and domain jargon. A fine-tuned distilbert-base-uncased-finetuned-sst-2-english model gives you better accuracy at the cost of slower inference:
| |
The tradeoff is speed. VADER processes thousands of documents per second on CPU. The transformer model needs batching and ideally a GPU to match that throughput.
Common Errors and Fixes
ValueError: could not convert string to float when building the FAISS index
This happens if you pass the raw output of model.encode() without converting to a float32 numpy array. FAISS requires float32 specifically:
| |
Empty results after sentiment filtering
If sentiment_aware_search returns an empty list, the candidate pool is too small relative to the sentiment distribution. Either increase fetch_k or relax sentiment_threshold. You can add a fallback:
| |
ImportError: No module named 'faiss'
The pip package name is faiss-cpu, not faiss. If you have a CUDA-capable GPU, install faiss-gpu instead:
| |
VADER scores everything as neutral (compound near 0)
VADER is trained on English social media text. If your documents are in another language, contain heavy jargon, or are very short (1-2 words), the scores will cluster around zero. For non-English text, switch to a multilingual sentiment model. For domain jargon, consider fine-tuning or using a domain-specific lexicon.
FAISS search() returns -1 indices
This means FAISS couldn’t find enough neighbors, usually because top_k exceeds the number of indexed documents. Check index.ntotal and make sure your fetch_k doesn’t exceed it:
| |
Related Guides
- How to Build a RAG Pipeline with Hugging Face Transformers v5
- How to Build a Hybrid Keyword and Semantic Search Pipeline
- How to Build a Semantic Search Engine with Embeddings
- How to Build a Multilingual NLP Pipeline with Sentence Transformers
- How to Build an Emotion Detection Pipeline with GoEmotions and Transformers
- How to Detect Duplicate and Similar Texts with Embeddings
- How to Build an Aspect-Based Sentiment Analysis Pipeline
- How to Build a Text Chunking and Splitting Pipeline for RAG
- How to Build a Text-to-Knowledge-Graph Pipeline with SpaCy and NetworkX
- How to Build a Text Entailment and Contradiction Detection Pipeline