Posted on
Artificial Intelligence

Embedding Models Explained

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Embedding Models Explained: Bring Semantic Search to Your Linux Shell

Ever grepped for something you know exists but couldn’t recall exact words? Embedding models turn that fuzzy intent into precise results. Instead of matching characters, embeddings capture meaning—so “How do I autostart a service?” finds docs about “systemd units,” even if the word “autostart” doesn’t appear.

In this post, you’ll learn what embeddings are, why they matter to Linux users, and how to build a fast, local semantic search for your own docs, logs, and shell history—entirely from the command line with a few small Python scripts.


What are embeddings (and why you should care)

  • An embedding model converts text into a numerical vector (e.g., 384 or 768 floats) that encodes semantic meaning.

  • Similar texts end up with similar vectors. You can measure similarity with cosine similarity or dot product.

  • Why it’s valid and useful on Linux:

    • Search beyond keywords: find “apache crash logs” that talk about “httpd segfaults”.
    • Faster incident triage: group near-duplicate errors or configs.
    • Smarter knowledge lookup: query your scripts, READMEs, and notes by intent.
    • Fuel for RAG (Retrieval-Augmented Generation): retrieve the right snippets to feed your LLM.

Install prerequisites (apt, dnf, zypper)

We’ll use Python, curl, and jq. Choose your distro’s package manager.

Debian/Ubuntu (apt):

sudo apt update
sudo apt install -y python3 python3-venv python3-pip curl jq git

Fedora/RHEL/CentOS Stream (dnf):

sudo dnf install -y python3 python3-pip python3-virtualenv curl jq git

openSUSE (zypper):

sudo zypper refresh
sudo zypper install -y python3 python3-pip python3-virtualenv curl jq git

Create and activate a virtual environment, then install libraries:

python3 -m venv .venv
. .venv/bin/activate
pip install -U pip
pip install sentence-transformers faiss-cpu tqdm

Note: faiss-cpu provides a fast vector index. If your platform lacks wheels, ensure build tools are installed via your package manager.


A real-world example: Semantic search over your docs and history

We’ll index a folder of text-like files (README, .txt, .md, .log, .conf, scripts) and then run semantic queries. You can also add your shell history to the corpus.

Step 1: Gather some text

mkdir -p ~/semantic-corpus
# Example sources
cp -r /usr/share/doc ~/semantic-corpus/doc 2>/dev/null || true
cp ~/.bash_history ~/semantic-corpus/bash_history.txt 2>/dev/null || true
# Add your own notes/projects
cp -r ~/projects/* ~/semantic-corpus/ 2>/dev/null || true

Step 2: Create an index script (build_index.py)

cat > build_index.py << 'PY'
import os, json, argparse, pathlib, re
from pathlib import Path
from tqdm import tqdm
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer

def read_text_file(path):
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            return f.read()
    except Exception:
        return ""

def chunk_text(text, size=700, overlap=100):
    text = re.sub(r'\s+', ' ', text).strip()
    chunks = []
    start = 0
    n = len(text)
    while start < n:
        end = min(start + size, n)
        chunk = text[start:end]
        if chunk:
            chunks.append(chunk)
        start = end - overlap
        if start < 0: start = 0
        if start >= n: break
    return chunks

def collect_files(root, exts):
    root = Path(root)
    files = []
    for p in root.rglob("*"):
        if p.is_file() and (p.suffix.lower() in exts or p.suffix == "" and p.name.endswith("_history.txt")):
            files.append(p)
    return files

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--data-dir", required=True, help="Directory to index")
    ap.add_argument("--index", default="index.faiss")
    ap.add_argument("--meta", default="meta.json")
    ap.add_argument("--model", default="sentence-transformers/all-MiniLM-L6-v2")
    ap.add_argument("--batch-size", type=int, default=64)
    ap.add_argument("--chunk-size", type=int, default=700)
    ap.add_argument("--chunk-overlap", type=int, default=100)
    ap.add_argument("--extensions", default=".txt,.md,.log,.conf,.ini,.yaml,.yml,.sh,.py,.rst")
    args = ap.parse_args()

    model = SentenceTransformer(args.model)
    exts = set([e.strip() for e in args.extensions.split(",") if e.strip()])

    files = collect_files(args.data_dir, exts)
    if not files:
        print("No files found to index.")
        return

    dim = model.get_sentence_embedding_dimension()
    index = faiss.IndexFlatIP(dim)  # cosine via normalized dot product
    metadata = []

    texts = []
    meta_buffer = []

    for fp in tqdm(files, desc="Reading & chunking"):
        text = read_text_file(fp)
        if not text:
            continue
        chunks = chunk_text(text, size=args.chunk_size, overlap=args.chunk_overlap)
        for i, ch in enumerate(chunks):
            texts.append(ch)
            meta_buffer.append({"path": str(fp), "chunk_id": i, "text": ch[:300]})

            # Encode in batches to save memory
            if len(texts) >= args.batch_size:
                embs = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
                index.add(embs.astype("float32"))
                metadata.extend(meta_buffer)
                texts, meta_buffer = [], []

    # Flush remainder
    if texts:
        embs = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
        index.add(embs.astype("float32"))
        metadata.extend(meta_buffer)

    faiss.write_index(index, args.index)
    with open(args.meta, "w", encoding="utf-8") as f:
        json.dump(metadata, f, ensure_ascii=False)
    print(f"Indexed {len(metadata)} chunks from {len(files)} files.")
    print(f"Saved index to {args.index} and metadata to {args.meta}")

if __name__ == "__main__":
    main()
PY

Step 3: Create a query script (search_index.py)

cat > search_index.py << 'PY'
import json, argparse
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--index", default="index.faiss")
    ap.add_argument("--meta", default="meta.json")
    ap.add_argument("--model", default="sentence-transformers/all-MiniLM-L6-v2")
    ap.add_argument("--k", type=int, default=5)
    ap.add_argument("--query", required=True)
    args = ap.parse_args()

    index = faiss.read_index(args.index)
    with open(args.meta, "r", encoding="utf-8") as f:
        meta = json.load(f)
    model = SentenceTransformer(args.model)

    q = model.encode([args.query], convert_to_numpy=True, normalize_embeddings=True).astype("float32")
    D, I = index.search(q, args.k)
    print(f"Top {args.k} results for: {args.query}\n")
    for rank, (idx, score) in enumerate(zip(I[0], D[0]), start=1):
        if idx == -1:
            continue
        m = meta[idx]
        print(f"[{rank}] score={score:.4f}")
        print(f"    file: {m['path']}")
        print(f"    snippet: {m['text']}\n")

if __name__ == "__main__":
    main()
PY

Step 4: Build and query the index

# Build
python3 build_index.py --data-dir ~/semantic-corpus --index ./myindex.faiss --meta ./mymeta.json

# Query examples
python3 search_index.py --index ./myindex.faiss --meta ./mymeta.json --query "how to create a systemd service" --k 5
python3 search_index.py --index ./myindex.faiss --meta ./mymeta.json --query "enable apache access logs" --k 5
python3 search_index.py --index ./myindex.faiss --meta ./mymeta.json --query "tar compress a folder and exclude files" --k 5

Optional: a shell helper for quick queries

semgrep() {
  . .venv/bin/activate >/dev/null 2>&1 || return 1
  python3 search_index.py --index ./myindex.faiss --meta ./mymeta.json --query "$*" --k 8
}
# Usage: semgrep find nginx vhost root

4 actionable tips to get better results

1) Pick the right model

  • General English: sentence-transformers/all-MiniLM-L6-v2 (fast, small).

  • Multilingual: paraphrase-multilingual-MiniLM-L12-v2.

  • Code-heavy repos: consider a code-aware embedding model from sentence-transformers’ model hub.

2) Normalize and use cosine

  • We used normalize_embeddings=True and FAISS IndexFlatIP. That gives cosine-similar behavior with good speed.

3) Chunk wisely

  • Too big: irrelevant text dilutes meaning. Too small: you lose context. Start with 500–800 characters and ~10–15% overlap.

  • For logs, group by N lines; for markdown, split by headings.

4) Keep your index fresh

  • Re-run the indexer in cron/systemd timers.

  • Store file mtime and skip unchanged files for incremental updates (easy enhancement).


Bonus: One-off embeddings via curl

If you prefer an API workflow (e.g., Hugging Face Inference Endpoints), you can obtain vectors with curl. You’ll need curl and jq (installed above) and a token in HF_TOKEN. Example template:

export HF_TOKEN=hf_XXXXXXXXXXXXXXXXXXXXXXXXXXXX

curl -s -H "Authorization: Bearer $HF_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"inputs": ["how to rotate nginx logs", "create systemd unit file"]}' \
     https://api-inference.huggingface.co/pipeline/feature-extraction/sentence-transformers/all-MiniLM-L6-v2 \
| jq

You can store returned embeddings and feed them into FAISS the same way.


Troubleshooting

  • faiss-cpu install issues: upgrade pip and retry. If wheels are unavailable on your arch, install build tools via your package manager and consider using an alternative vector store (e.g., numpy + sklearn NearestNeighbors) for small datasets.

  • Memory constraints: index fewer files at first, or switch to an IVF/Flat hybrid in FAISS for large corpora.

  • Performance: batch size impacts both memory and speed—tune based on your system.


Conclusion and next steps

Embeddings let you search by meaning, not memorized strings. In minutes, you built a local semantic search for your docs and history using sentence-transformers and FAISS.

Next steps:

  • Add incremental indexing and file change detection.

  • Extend search output to open files directly in your $EDITOR.

  • Try a domain-specific embedding model for your stack.

  • Feed top results into an LLM for on-demand “RAG” answers.

If this helped, share your setup or improvements—Bash-friendly semantic tooling is just getting started.