- Posted on
- • Artificial Intelligence
Building Artificial Intelligence Knowledge Bases
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Building Artificial Intelligence Knowledge Bases from the Linux Command Line
If your team’s know-how lives in scattered READMEs, PDFs, tickets, and wikis, you’re burning time hunting for answers. An AI-powered knowledge base turns that sprawl into a fast, searchable system that can even draft answers. The best part? You can build it locally with Bash and a few open-source tools—no vendor lock-in, and your data stays on your machines.
This guide shows you how to assemble a private, scriptable Retrieval-Augmented Generation (RAG) pipeline on Linux. You’ll normalize documents to plain text, embed them as vectors, index for fast search, and query them via CLI. We’ll keep it minimal, reproducible, and distro-friendly.
Why this matters
Faster answers, less context switching: Ask a question and pull relevant snippets in seconds.
Privacy by default: Keep sensitive docs on-prem with local embeddings and indices.
Repeatable and automatable: Cron, systemd, Git, and shell scripts keep it updated and auditable.
Extensible: Bolt on an LLM (local or remote) later to generate summarized answers.
What you’ll build
A text normalizer for PDFs, Office docs, images (OCR), HTML, Markdown
A Python-based embedder using Sentence Transformers
A FAISS vector index for approximate similarity search
A CLI to query the knowledge base, optionally piping context into an LLM like Ollama
0) Install prerequisites
Tools we’ll use:
Core: Python 3, pip/venv, Git
Converters: pandoc, poppler utilities (pdftotext), tesseract (OCR)
Helpers: ripgrep, jq
On Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y \
python3 python3-venv python3-pip git \
pandoc poppler-utils tesseract-ocr \
ripgrep jq
On Fedora/RHEL/CentOS (dnf):
# You may need EPEL for some packages on RHEL/CentOS:
# sudo dnf install -y epel-release
sudo dnf install -y \
python3 python3-pip git \
pandoc poppler-utils tesseract \
ripgrep jq
On openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y \
python3 python3-pip git \
pandoc poppler-tools tesseract \
ripgrep jq
Create a workspace and Python environment:
mkdir -p ~/kb/{data/raw,data/text,index,tools}
cd ~/kb
python3 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install "sentence-transformers>=2.6" "faiss-cpu>=1.8" "tqdm>=4.66" "numpy>=1.26"
1) Ingest and normalize your documents
Start by collecting raw files under data/raw. Examples:
# Example: pull internal wiki export or docs
# cp -r /mnt/share/runbooks/* data/raw/
# Example: clone public docs (replace with your repos)
git clone --depth=1 https://github.com/curl/curl.git data/raw/curl
Convert everything to plain text with a single Bash script. Save as tools/totext.sh:
#!/usr/bin/env bash
set -euo pipefail
in="${1:-data/raw}"
out="${2:-data/text}"
mkdir -p "$out"
shopt -s globstar nullglob
for f in "$in"/**/*; do
[ -f "$f" ] || continue
rel="${f#"$in"/}"
base="${rel%.*}"
ext="${f##*.}"
mkdir -p "$out/$(dirname "$rel")"
case "${ext,,}" in
txt|md|rst)
# Preserve structure
cp -f "$f" "$out/$rel"
;;
pdf)
pdftotext -layout "$f" "$out/$base.txt" || true
;;
docx|odt|pptx|xlsx|html|htm)
pandoc -s "$f" -t plain -o "$out/$base.txt" || true
;;
png|jpg|jpeg|tif|tiff)
# OCR images
tesseract "$f" "$out/$base" >/dev/null 2>&1 || true
;;
*)
# Unknown format: skip silently
:
;;
esac
done
# Quick sanity: list converted files
echo "Converted files:"
rg -nH --glob '!*.git/*' '' "$out" | head -n 10 || true
Make it executable and run:
chmod +x tools/totext.sh
bash tools/totext.sh data/raw data/text
Tip: Keep only what matters. Fewer, higher-quality docs beat noisy dumps.
2) Chunk and embed text (vectorization)
We’ll split text into overlapping chunks (good for retrieval) and embed with a compact, fast model.
Save as tools/index.py:
#!/usr/bin/env python3
import os, json, pathlib, glob
import numpy as np
from tqdm import tqdm
from sentence_transformers import SentenceTransformer
import faiss
TEXT_DIR = "data/text"
INDEX_DIR = "index"
META_PATH = os.path.join(INDEX_DIR, "meta.jsonl")
INDEX_PATH = os.path.join(INDEX_DIR, "faiss.index")
MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim
CHUNK_SIZE = 800 # characters
CHUNK_OVERLAP = 120 # characters
def chunk_text(s, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
s = s.strip()
if not s:
return []
chunks = []
start = 0
while start < len(s):
end = min(len(s), start + size)
chunks.append((start, s[start:end]))
start += size - overlap
if start <= 0: # avoid infinite loop when overlap >= size
break
return chunks
def load_docs():
patterns = ["**/*.txt", "**/*.md", "**/*.rst"]
files = []
for p in patterns:
files.extend(glob.glob(os.path.join(TEXT_DIR, p), recursive=True))
files = sorted(set(files))
docs = []
for f in files:
try:
text = pathlib.Path(f).read_text(encoding="utf-8", errors="ignore")
if text.strip():
docs.append((f, text))
except Exception:
pass
return docs
def main():
os.makedirs(INDEX_DIR, exist_ok=True)
model = SentenceTransformer(MODEL_NAME)
meta = []
vectors = []
for path, text in tqdm(load_docs(), desc="Chunking/encoding"):
for start, chunk in chunk_text(text):
end = start + len(chunk)
meta.append({"path": path, "start": start, "end": end, "preview": chunk[:240].replace("\n", " ")})
vectors.append(model.encode(chunk, normalize_embeddings=True)) # cosine-ready
if not vectors:
print("No text found to index. Did you run totext.sh?")
return
X = np.vstack(vectors).astype("float32")
dim = X.shape[1]
index = faiss.IndexFlatIP(dim) # inner product + normalized vectors = cosine
index.add(X)
faiss.write_index(index, INDEX_PATH)
with open(META_PATH, "w", encoding="utf-8") as f:
for m in meta:
f.write(json.dumps(m, ensure_ascii=False) + "\n")
print(f"Indexed {len(meta)} chunks into {INDEX_PATH}")
print(f"Metadata written to {META_PATH}")
if __name__ == "__main__":
main()
Run it:
python3 tools/index.py
Result:
index/faiss.index: your vector index
index/meta.jsonl: mapping of vectors to source files and locations
3) Query the knowledge base from Bash
A simple CLI that searches top-k chunks and prints sources. Optionally, send context to a local LLM (if you run one).
Save as tools/query.py:
#!/usr/bin/env python3
import os, json, sys, subprocess
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss
INDEX_PATH = "index/faiss.index"
META_PATH = "index/meta.jsonl"
MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
def load_meta():
meta = []
with open(META_PATH, "r", encoding="utf-8") as f:
for line in f:
meta.append(json.loads(line))
return meta
def answer_with_ollama(context, question, model):
prompt = f"""You are a helpful assistant. Use ONLY the context to answer.
Question: {question}
Context:
{context}
Answer:"""
try:
out = subprocess.run(
["ollama", "run", model],
input=prompt,
text=True,
check=True,
stdout=subprocess.PIPE
).stdout
return out.strip()
except Exception as e:
return f"(Ollama failed: {e})"
def main():
if len(sys.argv) < 2:
print("Usage: query.py 'your question here' [k]", file=sys.stderr)
sys.exit(1)
question = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv) > 2 else 5
index = faiss.read_index(INDEX_PATH)
meta = load_meta()
model = SentenceTransformer(MODEL_NAME)
q = model.encode(question, normalize_embeddings=True).astype("float32")
D, I = index.search(np.expand_dims(q, 0), k)
hits = [meta[i] for i in I[0] if i < len(meta)]
print("Top matches:")
for rank, h in enumerate(hits, 1):
print(f"{rank}. {h['path']} [{h['start']}:{h['end']}]")
print(f" {h['preview'][:200]}...")
print()
ollama_model = os.environ.get("OLLAMA_MODEL")
if ollama_model:
context = "\n\n".join([f"{h['path']}:\n{h['preview']}" for h in hits])
print("LLM answer (via Ollama):")
print(answer_with_ollama(context, question, ollama_model))
if __name__ == "__main__":
main()
Usage:
python3 tools/query.py "How do I rotate nginx logs?" 5
Optional: If you have a local LLM via Ollama, you can ask it to generate an answer from the retrieved context:
# Example: use a small local model
export OLLAMA_MODEL=llama3
python3 tools/query.py "How do I rotate nginx logs?"
Note: Ollama provides a Linux install script on their site. This guide doesn’t require it to run.
4) Real-world patterns that work
SRE/runbook KB: Point data/raw at your incident guides, postmortems, on-call docs, and service READMEs. Query “restart playbook for service X” or “what to check before failover?”
Compliance/legal: Use contract PDFs and policy docs. Ask for “termination clause summary” or “password policy specifics.”
Customer support: Index FAQ, troubleshooting trees, and release notes. Ask “why does feature Y fail after upgrade?”
Developer enablement: Index architecture docs, ADRs, code guidelines. Ask “how do we add a new microservice?” or “logging standards?”
Quality tips:
Chunk size 500–1,000 characters with 10–20% overlap works well.
Keep sources deduplicated; avoid machine-generated noise.
Make provenance visible by always showing file paths in answers.
5) Keep it fresh (automation)
Minimal cron job to re-run normalization and reindex nightly:
crontab -e
Add:
0 2 * * * cd /home/youruser/kb && . .venv/bin/activate && bash tools/totext.sh && python3 tools/index.py >> /var/log/kb-reindex.log 2>&1
You can also wire this into CI after docs change, or use a systemd timer for finer control.
Troubleshooting
No matches found:
- Check data/text has .txt output:
ls -R data/text | head - Ensure converters are installed (pandoc, poppler, tesseract).
- Check data/text has .txt output:
Index small or empty:
- Did totext.sh run before index.py?
- Are your docs binary or images without OCR?
Slow or memory heavy on huge corpora:
- Consider FAISS IVF/HNSW indexes for scalability.
- Shard by topic (one index per department) to keep queries snappy.
Wrap-up and next steps
You now have a private, scriptable AI knowledge base:
Normalize docs to plain text
Create embeddings and a vector index
Query with provenance
Optionally summarize with a local LLM
Next steps:
Add a tiny web UI that shells out to query.py
Enrich metadata (tags, owners) and filter by them
Add a reranker (e.g., cross-encoder) for higher answer quality
Gate PII and secrets before indexing
If this helped, try pointing it at your team’s most painful docs silo today. Ten minutes of setup can save hours of searching this week.