Agentic RAG over Alfresco without another vector database
Documents used by a RAG application often already live in Alfresco. The usual RAG architecture nevertheless starts by building another repository next to it. It crawls Alfresco, downloads every document, extracts the tex
Documents used by a RAG application often already live in Alfresco. The usual RAG architecture nevertheless starts by building another repository next to it. It crawls Alfresco, downloads every document, extracts the text, splits it into chunks, calculates embeddings and stores everything in a vector database.
For a demo with a few documents, this is easy enough. For an Alfresco repository with hundreds of thousands or millions of documents, the vector database becomes another system that needs to be operated.
The initial ingestion is only the beginning. New and changed documents must be processed. Deleted documents and their chunks must disappear. Failed conversions need to be retried. The vector database has to follow changes in Alfresco without quietly turning into a stale copy of the repository.
It also needs its own content extraction pipeline. Alfresco is normally already configured with transformers for the file formats stored in the repository. The new ingestion pipeline has to reproduce this configuration for PDF, Office, HTML, text and any less common formats used by the organisation.
The most difficult part, however, are permissions.
Copying the content also means copying its access rules
A RAG application must not retrieve a chunk if the current user is not allowed to read the source document in Alfresco.
This is more involved than adding an allowed_users field to each vector. ECM permissions can depend on users, groups, inherited permissions and explicit denies. A change to a group membership can alter access to many documents without changing these documents. Moving a document can change inherited permissions. An administrator can revoke access while an older copy of the chunks is still present in the vector store.
A stale content copy can produce an outdated answer. A stale permission copy can disclose information.
It is possible to reproduce the effective access rules in a vector database, but this is a separate synchronization problem. It also needs to be applied to every retrieval path. A metadata filter that is forgotten in one query is a security problem rather than a small bug in answer quality.
An alternative is to leave retrieval to Alfresco. Search for documents in the repository at request time, fetch the matching content and add only this content to the LLM context. Alfresco remains responsible for deciding what the user can find and read.
There is an important condition: the search and the subsequent content fetch must run in the security context of the current user. Using a shared service account, or the admin/admin credentials found in many tutorials, does not preserve end-user permissions. In a production setup, the caller must be authenticated and its identity must be propagated or translated through the OpenDMA endpoint to Alfresco.
Searching Alfresco through OpenDMA
OpenDMA defines a common API for Document Management and Enterprise Content Management systems. An OpenDMA Adaptor maps this API to a specific system, similar to a database driver in JDBC. The REST API makes an adaptor available to Python applications across the network.
The open-source LangChain integration contains an AlfrescoRetriever. Instead of searching a vector store, it creates an Alfresco Full Text Search (AFTS) query and runs it against Alfresco through an OpenDMA endpoint.
The following is the essential part. username and password are meant to come from the authenticated user context here:
from langchain_opendma import AlfrescoRetriever
from langchain_opendma.content_handlers import DoclingLoaderContentHandler
retriever = AlfrescoRetriever(
endpoint="https://eci.example.com/opendma/alfresco",
username=current_user.username,
password=current_user.alfresco_password,
repository_id="Alfresco",
sites=["engineering"],
content_handlers=[DoclingLoaderContentHandler()],
k=8,
)
documents = retriever.invoke("localisation")
AlfrescoRetriever searches complete repository documents. It then retrieves the matching content and uses the configured content handlers to turn it into LangChain Document chunks. The k parameter limits the chunks returned to the application. It is not a semantic similarity score or a limit over vectors.
The site restriction is optional. It is useful because an Alfresco repository contains more structure than a flat collection of chunks. Sites, paths, content types, aspects and other content metadata can be used to reduce the search scope. A question about a development project is more likely to be answered by the Engineering site than by searching the entire repository.
This matters even with semantic search. In the metadata-aware OpenDMA tutorials, adding a second site with similar material causes relevant meeting notes to drop out of the top vector results. The additional chunks are semantically close to the question, but belong to the wrong context. Site metadata restores some of the precision that gets lost in a repository-wide search.
Full-text search is not semantic search
A vector store has a real advantage: the question does not need to contain the same words as the text that answers it. Embeddings can retrieve related concepts despite a vocabulary mismatch.
Alfresco full-text search is lexical. The choice of search terms matters more. If the user asks:
What is the state of localisation of our new website design?
a search for the complete question may return nothing. The relevant meeting note might only contain the word localisation, or use the American spelling localization.
This is where an agentic workflow helps. It can inspect the available Alfresco sites, select a likely scope and perform several searches with different terms. It does not have to treat the user's original question as the one and only search query.
The search operation can be exposed to a LangChain agent as a small tool:
from langchain.tools import tool
SITES_IN_SCOPE = {"swsdp", "engineering"}
@tool
def search_content(query: str, site: str | None = None) -> str:
"""Search Alfresco content, optionally restricted to one site."""
if site and site not in SITES_IN_SCOPE:
return f"Site {site!r} is not in scope."
retriever = AlfrescoRetriever(
endpoint=OPENDMA_ENDPOINT,
username=current_user.username,
password=current_user.alfresco_password,
repository_id="Alfresco",
sites=[site] if site else None,
content_handlers=[content_handler],
k=8,
)
documents = retriever.invoke(query)
if not documents:
return "No document chunks matched this search."
return "\n\n---\n\n".join(
f"Title: {doc.metadata.get('opendma:Title')}\n"
f"Site: {doc.metadata.get('alfresco:Site')}\n"
f"Path: {doc.metadata.get('alfresco:Path')}\n"
f"Source: {doc.metadata.get('source')}\n"
f"Content:\n{doc.page_content}"
for doc in documents
)
The agent also receives a list_sites tool. This allows it to discover that a question about a website design probably belongs to the swsdp site before it searches the document content.
The relevant part of the orchestration prompt is deliberately simple:
Use search_content before answering factual questions about repository content.
Prefer a site-restricted search when a relevant Alfresco site is known.
The tool uses Alfresco full-text search, not semantic vector search. If a search
returns weak results, search again with a shorter, simpler or broader query.
Do not answer unless the answer is supported by retrieved context.
In one representative run from the tutorial, the agent takes these steps:
list_sites()
-> swsdp: Sample: Web Site Design Project
-> engineering: Engineering
search_content("localisation of new website design", site="swsdp")
-> no matching chunks
search_content("website design", site="swsdp")
-> no matching chunks
search_content("localisation", site="swsdp")
-> meeting notes: localisation was included in phase 1
The agent needed three attempts, but it found the information without a vector store. Another model or another run may choose different query terms. Even with temperature zero, hosted models change and an agentic trace should not be treated as deterministic output.
The complete workflow in the Agentic RAG using OpenDMA Retriever tutorial adds a retrieval assessment step. It grades the latest search result, gives the orchestrator feedback and limits the number of attempts. This prevents the agent from searching indefinitely or answering a repository question from its own model knowledge.
The runnable sample application implements this workflow as a Python command-line application. It includes the site restriction, bounded search retries and retrieval trace shown above. It uses Docling for content conversion by default; --unstructured selects Unstructured for formats such as legacy Word .doc files.
What changes without the vector database
The design removes an additional, persistent knowledge-base copy. It does not mean that no indexing, text extraction or temporary copying happens anywhere.
Alfresco normally uses its own search index. Search results are subject to the visibility and consistency behavior of that index. Retrieved documents still have to be converted to text, and the selected chunks are sent to the model as context. Framework or application caches may keep transformed content for some time.
What disappears is the repository-wide pipeline that continuously copies Alfresco content into a second search system:
| Concern | Direct Alfresco search | Separate vector store |
|---|---|---|
| Persistent content copy | No additional knowledge base | Chunks and embeddings are copied |
| Initial ingestion | Not required | Repository-wide extraction and embedding |
| Changes | Use the repository's searchable state | Synchronize creates, updates and deletes |
| Permissions | Enforced by Alfresco when the user identity is propagated | Effective access must be copied and applied during retrieval |
| Retrieval | Lexical search; the agent may need several attempts | Semantic similarity handles vocabulary differences better |
| Request cost | Live search and content conversion | Lower retrieval cost after ingestion |
This approach is a good fit when keeping Alfresco as the controlled system of record is more important than obtaining the highest possible semantic recall. It also works best when Alfresco search is properly configured and the application can tolerate a small, bounded number of search attempts.
A vector store can still be the right choice when semantic retrieval quality or predictable query latency dominates these concerns. A hybrid design is another option. Neither changes the permission requirement: if chunks are copied out of Alfresco, the application has to maintain and enforce the effective access rules for these chunks.
The OpenDMA retriever pattern is also available for LlamaIndex and Haystack. All three integrations use the same basic idea: search the ECM repository when the question is asked, use repository metadata to narrow the scope, and let an agent try another query when the first result is not sufficient.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.