This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-indexer.git


The following commit(s) were added to refs/heads/master by this push:
     new 1e532fd  [MINDEXER-143] Introduce Search API and backends (#197)
1e532fd is described below

commit 1e532fdc8624cb5798798e9f52641015a9894aa5
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Fri Apr 22 10:35:38 2022 +0200

    [MINDEXER-143] Introduce Search API and backends (#197)
    
    Introduce Search API and provide back-ends.
    
    Introduced 3 new modules: search-api and two back-ends: indexer based one 
and SMO based one.
---
 pom.xml                                            |  21 ++
 search-api/README.md                               |  24 ++
 search-api/header.txt                              |  17 ++
 search-api/pom.xml                                 |  51 ++++
 .../main/java/org/apache/maven/search/MAVEN.java   | 103 ++++++++
 .../main/java/org/apache/maven/search/Record.java  | 139 ++++++++++
 .../org/apache/maven/search/SearchBackend.java     |  38 +++
 .../java/org/apache/maven/search/SearchEngine.java |  33 +++
 .../org/apache/maven/search/SearchRequest.java     |  82 ++++++
 .../org/apache/maven/search/SearchResponse.java    |  49 ++++
 .../apache/maven/search/request/BooleanQuery.java  |  87 +++++++
 .../org/apache/maven/search/request/Field.java     | 132 ++++++++++
 .../apache/maven/search/request/FieldQuery.java    |  62 +++++
 .../org/apache/maven/search/request/Paging.java    |  85 +++++++
 .../org/apache/maven/search/request/Query.java     |  57 +++++
 .../maven/search/support/SearchBackendSupport.java |  58 +++++
 .../search/support/SearchResponseSupport.java      |  78 ++++++
 search-backend-indexer/README.md                   |  14 +
 search-backend-indexer/header.txt                  |  17 ++
 search-backend-indexer/pom.xml                     |  80 ++++++
 .../backend/indexer/IndexerCoreSearchBackend.java  |  34 +++
 .../backend/indexer/IndexerCoreSearchResponse.java |  42 +++
 .../internal/IndexerCoreSearchBackendImpl.java     | 226 ++++++++++++++++
 .../internal/IndexerCoreSearchResponseImpl.java    |  61 +++++
 .../internal/IndexerCoreSearchBackendImplTest.java | 283 +++++++++++++++++++++
 search-backend-smo/README.md                       |  13 +
 search-backend-smo/header.txt                      |  17 ++
 search-backend-smo/pom.xml                         |  66 +++++
 .../main/filtered-resources/smo-version.properties |  18 ++
 .../maven/search/backend/smo/SmoSearchBackend.java |  33 +++
 .../search/backend/smo/SmoSearchResponse.java      |  38 +++
 .../backend/smo/internal/SmoSearchBackendImpl.java | 235 +++++++++++++++++
 .../smo/internal/SmoSearchResponseImpl.java        |  56 ++++
 .../smo/internal/SmoSearchTransportSupport.java    |  81 ++++++
 .../internal/UrlConnectionSmoSearchTransport.java  |  59 +++++
 .../smo/internal/SmoSearchBackendImplTest.java     | 172 +++++++++++++
 36 files changed, 2661 insertions(+)

diff --git a/pom.xml b/pom.xml
index 6f3a46e..bb34134 100644
--- a/pom.xml
+++ b/pom.xml
@@ -106,6 +106,12 @@ under the License.
   <dependencyManagement>
     <dependencies>
       <!-- Reactor -->
+      <dependency>
+        <groupId>org.apache.maven.indexer</groupId>
+        <artifactId>search-api</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.maven.indexer</groupId>
         <artifactId>indexer-core</artifactId>
@@ -285,10 +291,25 @@ under the License.
     <module>indexer-cli</module>
     <module>indexer-reader</module>
     <module>indexer-examples</module>
+    <module>search-api</module>
+    <module>search-backend-indexer</module>
+    <module>search-backend-smo</module>
   </modules>
 
   <build>
     <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>3.0.0</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>extra-enforcer-rules</artifactId>
+            <version>1.4</version>
+          </dependency>
+        </dependencies>
+      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
diff --git a/search-api/README.md b/search-api/README.md
new file mode 100644
index 0000000..6205a7a
--- /dev/null
+++ b/search-api/README.md
@@ -0,0 +1,24 @@
+Indexer Search API
+==================
+
+Defines a simple Search API usable to most common Maven related searches.
+
+Example of GA search:
+
+```java
+    // obtain some backend instance
+    SearchBackend backend = ...
+
+    SearchRequest searchRequest = new SearchRequest( and( 
+            fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ),
+            fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ) )
+    );
+    SearchResponse searchResponse = backend.search( searchRequest );
+    process( searchResponse.getPage() ); // here consume the page data
+    while ( searchResponse.getCurrentHits() > 0 ) // if ALL needed, page 
through it
+    {
+        searchResponse = backend.search( 
searchResponse.getSearchRequest().nextPage() );
+        process( searchResponse.getPage() );
+    }
+```
+
diff --git a/search-api/header.txt b/search-api/header.txt
new file mode 100644
index 0000000..1a2ef73
--- /dev/null
+++ b/search-api/header.txt
@@ -0,0 +1,17 @@
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+
diff --git a/search-api/pom.xml b/search-api/pom.xml
new file mode 100644
index 0000000..a143cac
--- /dev/null
+++ b/search-api/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.indexer</groupId>
+    <artifactId>maven-indexer</artifactId>
+    <version>6.1.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>search-api</artifactId>
+
+  <name>Maven :: Search API</name>
+  <description>
+    Indexer Search API.
+  </description>
+
+  <dependencies>
+    <!-- Test -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/search-api/src/main/java/org/apache/maven/search/MAVEN.java 
b/search-api/src/main/java/org/apache/maven/search/MAVEN.java
new file mode 100644
index 0000000..99f3cb4
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/MAVEN.java
@@ -0,0 +1,103 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.search.request.Field.BooleanField;
+import org.apache.maven.search.request.Field.NumberField;
+import org.apache.maven.search.request.Field.StringField;
+
+/**
+ * The ontology of Apache Maven related fields.
+ */
+public final class MAVEN
+{
+    private MAVEN()
+    {
+        // no instances
+    }
+
+    /**
+     * String field for artifact groupId. Searchable.
+     */
+    public static final StringField GROUP_ID = new StringField( "groupId", 
true );
+
+    /**
+     * String field for artifact artifactId. Searchable.
+     */
+    public static final StringField ARTIFACT_ID = new StringField( 
"artifactId", true );
+
+    /**
+     * String field for artifact version. Searchable.
+     */
+    public static final StringField VERSION = new StringField( "version", true 
);
+
+    /**
+     * String field for artifact classifier. Searchable.
+     */
+    public static final StringField CLASSIFIER = new StringField( 
"classifier", true );
+
+    /**
+     * String field for artifact packaging. Searchable.
+     */
+    public static final StringField PACKAGING = new StringField( "packaging", 
true );
+
+    /**
+     * String field for artifact contained Java class name. Searchable, but 
not present in resulting records.
+     */
+    public static final StringField CLASS_NAME = new StringField( "cn", true );
+
+    /**
+     * String field for artifact contained FQ Java class name. Searchable, but 
not present in resulting records.
+     */
+    public static final StringField FQ_CLASS_NAME = new StringField( "fqcn", 
true );
+
+    /**
+     * String field for artifact SHA1 checksum. Searchable, but not present in 
resulting records.
+     */
+    public static final StringField SHA1 = new StringField( "sha1", true );
+
+    /**
+     * String field for artifact file extension. Non-searchable. Indexer 
backend specific.
+     */
+    public static final StringField FILE_EXTENSION = new StringField( 
"fileExtension", false );
+
+    /**
+     * Number field carrying {@link Integer}, representing the count of 
versions for given GA. Non-searchable.
+     */
+    public static final NumberField VERSION_COUNT = new NumberField( 
"versionCount", false );
+
+    /**
+     * Boolean field representing the known presence/absence of artifact 
sources (is {@code -sources.jar} present).
+     * Non-searchable.
+     */
+    public static final BooleanField HAS_SOURCE = new BooleanField( "source", 
false );
+
+    /**
+     * Boolean field representing the known presence/absence of artifact 
Javadoc (is {@code -javadoc.jar} present).
+     * Non-searchable.
+     */
+    public static final BooleanField HAS_JAVADOC = new BooleanField( 
"javadoc", false );
+
+    /**
+     * Boolean field representing the known presence/absence of artifact GPG 
signature. Non-searchable. Indexer
+     * backend specific.
+     */
+    public static final BooleanField HAS_GPG_SIGNATURE = new BooleanField( 
"gpg", false );
+}
diff --git a/search-api/src/main/java/org/apache/maven/search/Record.java 
b/search-api/src/main/java/org/apache/maven/search/Record.java
new file mode 100644
index 0000000..0f99b0e
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/Record.java
@@ -0,0 +1,139 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.search.request.Field;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A search response record.
+ */
+public final class Record
+{
+    private final String backendId;
+
+    private final String repositoryId;
+
+    private final String uid;
+
+    private final Long lastUpdated;
+
+    private final Map<Field, Object> fields;
+
+    public Record( String backendId,
+                   String repositoryId,
+                   String uid,
+                   Long lastUpdated,
+                   Map<Field, Object> fields )
+    {
+        this.backendId = requireNonNull( backendId );
+        this.repositoryId = requireNonNull( repositoryId );
+        this.uid = uid;
+        this.lastUpdated = lastUpdated;
+        this.fields = Collections.unmodifiableMap( fields );
+    }
+
+    /**
+     * Returns {@link SearchBackend#getBackendId()} of originating search 
backend. Never {@code null}.
+     */
+    public String getBackendId()
+    {
+        return backendId;
+    }
+
+    /**
+     * Returns {@link SearchBackend#getRepositoryId()}) of originating search 
backend. Never {@code null}.
+     */
+    public String getRepositoryId()
+    {
+        return repositoryId;
+    }
+
+    /**
+     * Returns UID (unique if combined with {@link #getBackendId()}) of search 
result record, if provided by backend.
+     * May be {@code null} if not provided.
+     */
+    public String getUid()
+    {
+        return uid;
+    }
+
+    /**
+     * Returns {@link Long}, representing "last updated" timestamp as epoch 
millis if provided by backend. May be
+     * {@code null} if not provided.
+     */
+    public Long getLastUpdated()
+    {
+        return lastUpdated;
+    }
+
+    /**
+     * Returns unmodifiable map of all values keyed by {@link Field} backing 
this record.
+     */
+    public Map<Field, Object> getFields()
+    {
+        return fields;
+    }
+
+    /**
+     * Returns unmodifiable set of present fields in this record, never {@code 
null}.
+     */
+    public Set<Field> fieldSet()
+    {
+        return fields.keySet();
+    }
+
+    /**
+     * Returns {@code true} if given field is present in this record.
+     */
+    public boolean hasField( Field field )
+    {
+        return fields.containsKey( field );
+    }
+
+    /**
+     * Returns the value belonging to given field in this record, or {@code 
null} if field not present.
+     */
+    public String getValue( Field.StringField field )
+    {
+        return field.getFieldValue( fields );
+    }
+
+    /**
+     * Returns the value belonging to given field in this record, or {@code 
null} if field not present.
+     */
+    public Number getValue( Field.NumberField field )
+    {
+        return field.getFieldValue( fields );
+    }
+
+    /**
+     * Returns the value belonging to given field in this record, or {@code 
null} if field not present.
+     */
+    public Boolean getValue( Field.BooleanField field )
+    {
+        return field.getFieldValue( fields );
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/SearchBackend.java 
b/search-api/src/main/java/org/apache/maven/search/SearchBackend.java
new file mode 100644
index 0000000..68fecd3
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/SearchBackend.java
@@ -0,0 +1,38 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.Closeable;
+
+/**
+ * An engine to perform search trough single repository index (backend).
+ */
+public interface SearchBackend extends SearchEngine, Closeable
+{
+    /**
+     * Returns the ID of this backend, never {@code null}.
+     */
+    String getBackendId();
+
+    /**
+     * Returns the repository ID that this backend searches for, never {@code 
null}.
+     */
+    String getRepositoryId();
+}
diff --git a/search-api/src/main/java/org/apache/maven/search/SearchEngine.java 
b/search-api/src/main/java/org/apache/maven/search/SearchEngine.java
new file mode 100644
index 0000000..cd82039
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/SearchEngine.java
@@ -0,0 +1,33 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+
+/**
+ * A search engine to perform searches trough configured repository indexes.
+ */
+public interface SearchEngine
+{
+    /**
+     * Performs a search with given {@link SearchRequest} and returns {@link 
SearchResponse}, never {@code null}.
+     */
+    SearchResponse search( SearchRequest searchRequest ) throws IOException;
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/SearchRequest.java 
b/search-api/src/main/java/org/apache/maven/search/SearchRequest.java
new file mode 100644
index 0000000..c71fde8
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/SearchRequest.java
@@ -0,0 +1,82 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.search.request.Paging;
+import org.apache.maven.search.request.Query;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A search request to perform search: defines paging and query.
+ */
+public final class SearchRequest
+{
+    private final Paging paging;
+
+    private final Query query;
+
+    /**
+     * Creates a request with given {@link Query} instance and default page 
size of 50.
+     */
+    public SearchRequest( Query query )
+    {
+        this( new Paging( 50 ), query );
+    }
+
+    /**
+     * Creates a request with given {@link Query} and {@link Paging}.
+     */
+    public SearchRequest( Paging paging, Query query )
+    {
+        this.paging = requireNonNull( paging );
+        this.query = requireNonNull( query );
+    }
+
+    /**
+     * The {@link Paging} of this request: defines page size and page offset, 
never {@code null}.
+     */
+    public Paging getPaging()
+    {
+        return paging;
+    }
+
+    /**
+     * The {@link Query} of this request, never {@code null}.
+     */
+    public Query getQuery()
+    {
+        return query;
+    }
+
+    /**
+     * Returns a new {@link SearchRequest} instance for "next page" relative 
to this instance, never {@code null}.
+     */
+    public SearchRequest nextPage()
+    {
+        return new SearchRequest( paging.nextPage(), query );
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + "{" + "paging=" + paging + ", 
query=" + query + '}';
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/SearchResponse.java 
b/search-api/src/main/java/org/apache/maven/search/SearchResponse.java
new file mode 100644
index 0000000..969b10a
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/SearchResponse.java
@@ -0,0 +1,49 @@
+package org.apache.maven.search;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+/**
+ * A search engine response.
+ */
+public interface SearchResponse
+{
+    /**
+     * Returns the {@link SearchRequest} used for this response, never {@code 
null}.
+     */
+    SearchRequest getSearchRequest();
+
+    /**
+     * Returns the total count of hits produced by {@link #getSearchRequest()}.
+     */
+    int getTotalHits();
+
+    /**
+     * Returns the count of current hits in current "page". It may be less or 
equal to page size of {@link
+     * SearchRequest#getPaging()}.
+     */
+    int getCurrentHits();
+
+    /**
+     * Returns current "page" of results as list of records, never {@code 
null}.
+     */
+    List<Record> getPage();
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java 
b/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java
new file mode 100644
index 0000000..0a4619d
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java
@@ -0,0 +1,87 @@
+package org.apache.maven.search.request;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Boolean query.
+ */
+public abstract class BooleanQuery extends Query
+{
+    private final Query left;
+
+    private final Query right;
+
+    protected BooleanQuery( Query left, String op, Query right )
+    {
+        super( op );
+        this.left = requireNonNull( left );
+        this.right = requireNonNull( right );
+    }
+
+    /**
+     * Returns left term of this boolean query, never {@code null}.
+     */
+    public Query getLeft()
+    {
+        return left;
+    }
+
+    /**
+     * Returns right term of this boolean query, never {@code null}.
+     */
+    public Query getRight()
+    {
+        return right;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getLeft() + " " + getValue() + " " + getRight();
+    }
+
+    public static final class And extends BooleanQuery
+    {
+        private And( Query left, Query right )
+        {
+            super( left, "AND", right );
+        }
+    }
+
+    /**
+     * Creates Logical AND query (requires presence of all queries) out of 
passed in queries (at least 2 or more
+     * should be given).
+     */
+    public static BooleanQuery and( Query left, Query... rights )
+    {
+        if ( rights.length == 0 )
+        {
+            throw new IllegalArgumentException( "one or more on right needed" 
);
+        }
+        BooleanQuery result = null;
+        for ( Query right : rights )
+        {
+            result = new And( result == null ? left : result, right );
+        }
+        return result;
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/request/Field.java 
b/search-api/src/main/java/org/apache/maven/search/request/Field.java
new file mode 100644
index 0000000..558a676
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/request/Field.java
@@ -0,0 +1,132 @@
+package org.apache.maven.search.request;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Map;
+import java.util.Objects;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Field, that is used as key in record.
+ */
+public abstract class Field
+{
+    private final String fieldName;
+
+    private final boolean searchable;
+
+    private Field( String fieldName, boolean searchable )
+    {
+        this.fieldName = requireNonNull( fieldName );
+        this.searchable = searchable;
+    }
+
+    /**
+     * Returns the field name.
+     */
+    public String getFieldName()
+    {
+        return fieldName;
+    }
+
+    /**
+     * Returns {@code true} if field may be used for {@link FieldQuery} (is 
searchable).
+     */
+    public boolean isSearchable()
+    {
+        return searchable;
+    }
+
+    /**
+     * Returns the value of the field from given record instance, or {@code 
null} if field not present in record.
+     * See subclasses for proper return types.
+     */
+    public abstract Object getFieldValue( Map<Field, Object> record );
+
+    @Override
+    public boolean equals( Object o )
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+        Field fieldName1 = (Field) o;
+        return Objects.equals( getFieldName(), fieldName1.getFieldName() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash( getFieldName() );
+    }
+
+    @Override
+    public String toString()
+    {
+        return fieldName;
+    }
+
+    public static class StringField extends Field
+    {
+        public StringField( String fieldName, boolean searchable )
+        {
+            super( fieldName, searchable );
+        }
+
+        @Override
+        public String getFieldValue( Map<Field, Object> record )
+        {
+            return (String) record.get( this );
+        }
+    }
+
+    public static class NumberField extends Field
+    {
+        public NumberField( String fieldName, boolean searchable )
+        {
+            super( fieldName, searchable );
+        }
+
+        @Override
+        public Number getFieldValue( Map<Field, Object> record )
+        {
+            return (Number) record.get( this );
+        }
+    }
+
+    public static class BooleanField extends Field
+    {
+        public BooleanField( String fieldName, boolean searchable )
+        {
+            super( fieldName, searchable );
+        }
+
+        @Override
+        public Boolean getFieldValue( Map<Field, Object> record )
+        {
+            return (Boolean) record.get( this );
+        }
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java 
b/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java
new file mode 100644
index 0000000..63f8497
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java
@@ -0,0 +1,62 @@
+package org.apache.maven.search.request;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Field query.
+ */
+public class FieldQuery extends Query
+{
+    private final Field field;
+
+    protected FieldQuery( Field field, String queryString )
+    {
+        super( queryString );
+        if ( !field.isSearchable() )
+        {
+            throw new IllegalArgumentException( "Field is not searchable: " + 
field );
+        }
+        this.field = requireNonNull( field );
+    }
+
+    /**
+     * Returns the field, never {@code null}.
+     */
+    public Field getField()
+    {
+        return field;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getField().getFieldName() + ":" + getValue();
+    }
+
+    /**
+     * Creates a field query using given {@link Field} and query string.
+     */
+    public static FieldQuery fieldQuery( Field fieldName, String query )
+    {
+        return new FieldQuery( fieldName, query );
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/request/Paging.java 
b/search-api/src/main/java/org/apache/maven/search/request/Paging.java
new file mode 100644
index 0000000..290a3b2
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/request/Paging.java
@@ -0,0 +1,85 @@
+package org.apache.maven.search.request;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Paging.
+ */
+public final class Paging
+{
+    private final int pageSize;
+
+    private final int pageOffset;
+
+    /**
+     * Creates paging instance with given page size (must be greater than 0) 
and page offset (must be non-negative).
+     */
+    public Paging( int pageSize, int pageOffset )
+    {
+        if ( pageSize < 1 )
+        {
+            throw new IllegalArgumentException( "pageSize" );
+        }
+        if ( pageOffset < 0 )
+        {
+            throw new IllegalArgumentException( "pageOffset" );
+        }
+        this.pageSize = pageSize;
+        this.pageOffset = pageOffset;
+    }
+
+    /**
+     * Creates paging instance with given page size (must be grater than 0) 
and 0 page offset.
+     */
+    public Paging( int pageSize )
+    {
+        this( pageSize, 0 );
+    }
+
+    /**
+     * Returns the page size: positive integer, never zero or less.
+     */
+    public int getPageSize()
+    {
+        return pageSize;
+    }
+
+    /**
+     * Returns the page offset: a zero or a positive integer.
+     */
+    public int getPageOffset()
+    {
+        return pageOffset;
+    }
+
+    /**
+     * Creates "next page" instance relative to this instance.
+     */
+    public Paging nextPage()
+    {
+        return new Paging( pageSize, pageOffset + 1 );
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + "{pageSize=" + pageSize + ", 
pageOffset=" + pageOffset + "}";
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/request/Query.java 
b/search-api/src/main/java/org/apache/maven/search/request/Query.java
new file mode 100644
index 0000000..35ddc59
--- /dev/null
+++ b/search-api/src/main/java/org/apache/maven/search/request/Query.java
@@ -0,0 +1,57 @@
+package org.apache.maven.search.request;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Query.
+ */
+public class Query
+{
+    private final String queryString;
+
+    protected Query( String queryString )
+    {
+        this.queryString = requireNonNull( queryString );
+    }
+
+    /**
+     * Returns the query string value, never {@code null}.
+     */
+    public String getValue()
+    {
+        return queryString;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getValue();
+    }
+
+    /**
+     * Creates a plain query.
+     */
+    public static Query query( String queryString )
+    {
+        return new Query( queryString );
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java
 
b/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java
new file mode 100644
index 0000000..7697942
--- /dev/null
+++ 
b/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java
@@ -0,0 +1,58 @@
+package org.apache.maven.search.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.search.SearchBackend;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A search backend support class.
+ */
+public abstract class SearchBackendSupport implements SearchBackend
+{
+    private final String backendId;
+
+    private final String repositoryId;
+
+    protected SearchBackendSupport( String backendId, String repositoryId )
+    {
+        this.backendId = requireNonNull( backendId );
+        this.repositoryId = requireNonNull( repositoryId );
+    }
+
+    @Override
+    public String getBackendId()
+    {
+        return backendId;
+    }
+
+    @Override
+    public String getRepositoryId()
+    {
+        return repositoryId;
+    }
+
+    @Override
+    public void close()
+    {
+        // override if needed
+    }
+}
diff --git 
a/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java
 
b/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java
new file mode 100644
index 0000000..ff8fa35
--- /dev/null
+++ 
b/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java
@@ -0,0 +1,78 @@
+package org.apache.maven.search.support;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.SearchResponse;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A search response support class.
+ */
+public abstract class SearchResponseSupport implements SearchResponse
+{
+    private final SearchRequest searchRequest;
+
+    private final int totalHits;
+
+    private final List<Record> page;
+
+    protected SearchResponseSupport( SearchRequest searchRequest, int 
totalHits, List<Record> page )
+    {
+        this.searchRequest = requireNonNull( searchRequest );
+        this.totalHits = totalHits;
+        this.page = requireNonNull( page );
+    }
+
+    @Override
+    public SearchRequest getSearchRequest()
+    {
+        return searchRequest;
+    }
+
+    @Override
+    public int getTotalHits()
+    {
+        return totalHits;
+    }
+
+    @Override
+    public int getCurrentHits()
+    {
+        return page.size();
+    }
+
+    @Override
+    public List<Record> getPage()
+    {
+        return page;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + "{" + "searchRequest=" + 
searchRequest + ", totalHits=" + totalHits
+                + ", page=" + page + '}';
+    }
+}
diff --git a/search-backend-indexer/README.md b/search-backend-indexer/README.md
new file mode 100644
index 0000000..e4c3116
--- /dev/null
+++ b/search-backend-indexer/README.md
@@ -0,0 +1,14 @@
+Indexer Search Indexer Backend
+==============================
+
+Search API Indexer Core Backend implementation.
+
+Examples:
+
+```java
+  // obtain Indexer instance
+  Indexer indexer = ...
+  IndexingContext indexingContext = ...
+
+  SearchBackend backend = new IndexerCoreSearchBackendImpl( indexer, 
indexingContext );
+```
\ No newline at end of file
diff --git a/search-backend-indexer/header.txt 
b/search-backend-indexer/header.txt
new file mode 100644
index 0000000..1a2ef73
--- /dev/null
+++ b/search-backend-indexer/header.txt
@@ -0,0 +1,17 @@
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+
diff --git a/search-backend-indexer/pom.xml b/search-backend-indexer/pom.xml
new file mode 100644
index 0000000..cf0643d
--- /dev/null
+++ b/search-backend-indexer/pom.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.indexer</groupId>
+    <artifactId>maven-indexer</artifactId>
+    <version>6.1.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>search-backend-indexer</artifactId>
+
+  <name>Maven :: Search API Indexer Backend</name>
+  <description>
+    Indexer Search Backend implemented by Indexer Core.
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.indexer</groupId>
+      <artifactId>search-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.maven.indexer</groupId>
+      <artifactId>indexer-core</artifactId>
+    </dependency>
+
+    <!-- Test -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.sisu</groupId>
+      <artifactId>org.eclipse.sisu.plexus</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.maven.wagon</groupId>
+      <artifactId>wagon-http-lightweight</artifactId>
+      <version>${wagon.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git 
a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java
 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java
new file mode 100644
index 0000000..9b9843a
--- /dev/null
+++ 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java
@@ -0,0 +1,34 @@
+package org.apache.maven.search.backend.indexer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.index.context.IndexingContext;
+import org.apache.maven.search.SearchBackend;
+
+/**
+ * The Indexer Core search backend.
+ */
+public interface IndexerCoreSearchBackend extends SearchBackend
+{
+    /**
+     * Returns the {@link IndexingContext} used by this search backend, never 
{@code null}.
+     */
+    IndexingContext getIndexingContext();
+}
diff --git 
a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java
 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java
new file mode 100644
index 0000000..727b526
--- /dev/null
+++ 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java
@@ -0,0 +1,42 @@
+package org.apache.maven.search.backend.indexer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.apache.lucene.search.Query;
+import org.apache.maven.index.ArtifactInfo;
+import org.apache.maven.search.SearchResponse;
+
+/**
+ * The Indexer Core search response.
+ */
+public interface IndexerCoreSearchResponse extends SearchResponse
+{
+    /**
+     * Returns the Lucene query used to create this response, never {@code 
null}.
+     */
+    Query getQuery();
+
+    /**
+     * Returns the "raw" list of {@link ArtifactInfo}s used to create this 
response, never {@code null}.
+     */
+    List<ArtifactInfo> getArtifactInfos();
+}
diff --git 
a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java
 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java
new file mode 100644
index 0000000..62ec217
--- /dev/null
+++ 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java
@@ -0,0 +1,226 @@
+package org.apache.maven.search.backend.indexer.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.StreamSupport;
+
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.apache.maven.index.ArtifactAvailability;
+import org.apache.maven.index.ArtifactInfo;
+import org.apache.maven.index.GroupedSearchRequest;
+import org.apache.maven.index.GroupedSearchResponse;
+import org.apache.maven.index.Indexer;
+import org.apache.maven.index.IteratorSearchRequest;
+import org.apache.maven.index.IteratorSearchResponse;
+import org.apache.maven.index.SearchType;
+import org.apache.maven.index.context.IndexingContext;
+import org.apache.maven.index.expr.SourcedSearchExpression;
+import org.apache.maven.index.search.grouping.GAGrouping;
+import org.apache.maven.search.MAVEN;
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.backend.indexer.IndexerCoreSearchBackend;
+import org.apache.maven.search.backend.indexer.IndexerCoreSearchResponse;
+import org.apache.maven.search.request.Field;
+import org.apache.maven.search.request.Paging;
+import org.apache.maven.search.support.SearchBackendSupport;
+import org.apache.maven.search.request.FieldQuery;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * An engine to perform search trough single repository index (endpoint).
+ */
+public class IndexerCoreSearchBackendImpl extends SearchBackendSupport 
implements IndexerCoreSearchBackend
+{
+    private static final Map<Field, org.apache.maven.index.Field> 
FIELD_TRANSLATION;
+
+    static
+    {
+        HashMap<Field, org.apache.maven.index.Field> map = new HashMap<>();
+        map.put( MAVEN.GROUP_ID, org.apache.maven.index.MAVEN.GROUP_ID );
+        map.put( MAVEN.ARTIFACT_ID, org.apache.maven.index.MAVEN.ARTIFACT_ID );
+        map.put( MAVEN.VERSION, org.apache.maven.index.MAVEN.VERSION );
+        map.put( MAVEN.CLASSIFIER, org.apache.maven.index.MAVEN.CLASSIFIER );
+        map.put( MAVEN.PACKAGING, org.apache.maven.index.MAVEN.PACKAGING );
+        map.put( MAVEN.CLASS_NAME, org.apache.maven.index.MAVEN.CLASSNAMES );
+        map.put( MAVEN.FQ_CLASS_NAME, org.apache.maven.index.MAVEN.CLASSNAMES 
);
+        map.put( MAVEN.SHA1, org.apache.maven.index.MAVEN.SHA1 );
+        FIELD_TRANSLATION = Collections.unmodifiableMap( map );
+    }
+
+    private final Indexer indexer;
+
+    private final IndexingContext indexingContext;
+
+    /**
+     * Creates backend instance using provided indexer and context.
+     */
+    public IndexerCoreSearchBackendImpl( Indexer indexer, IndexingContext 
indexingContext )
+    {
+        super( indexingContext.getId(), indexingContext.getRepositoryId() );
+        this.indexer = requireNonNull( indexer );
+        this.indexingContext = indexingContext;
+    }
+
+    @Override
+    public IndexingContext getIndexingContext()
+    {
+        return indexingContext;
+    }
+
+    @Override
+    public IndexerCoreSearchResponse search( SearchRequest searchRequest ) 
throws IOException
+    {
+        Paging paging = searchRequest.getPaging();
+        int totalHitsCount;
+        List<ArtifactInfo> artifactInfos = new ArrayList<>( 
paging.getPageSize() );
+        List<Record> page = new ArrayList<>( paging.getPageSize() );
+
+        // if GA present in query: doing flat, otherwise grouped search to 
mimic SMO
+        HashSet<Field> searchedFields = new HashSet<>();
+        Query query = toQuery( searchedFields, searchRequest.getQuery() );
+        if ( searchedFields.contains( MAVEN.SHA1 ) || ( 
searchedFields.contains( MAVEN.GROUP_ID )
+                && searchedFields.contains( MAVEN.ARTIFACT_ID ) ) )
+        {
+            if ( !searchedFields.contains( MAVEN.CLASSIFIER ) )
+            {
+                query = new BooleanQuery.Builder().add( new BooleanClause( 
query, BooleanClause.Occur.MUST ) )
+                        .add( indexer.constructQuery( 
org.apache.maven.index.MAVEN.CLASSIFIER,
+                                        new SourcedSearchExpression( 
org.apache.maven.index.Field.NOT_PRESENT ) ),
+                                BooleanClause.Occur.MUST_NOT ).build();
+            }
+            IteratorSearchRequest iteratorSearchRequest =
+                    new IteratorSearchRequest( query, 
Collections.singletonList( indexingContext ) );
+            iteratorSearchRequest.setCount( paging.getPageSize() );
+            iteratorSearchRequest.setStart( paging.getPageSize() * 
paging.getPageOffset() );
+
+            try ( IteratorSearchResponse iteratorSearchResponse = 
indexer.searchIterator( iteratorSearchRequest ) )
+            {
+                totalHitsCount = iteratorSearchResponse.getTotalHitsCount();
+                StreamSupport.stream( 
iteratorSearchResponse.iterator().spliterator(), false )
+                        .sorted( ArtifactInfo.VERSION_COMPARATOR ).forEach( ai 
->
+                        {
+                            artifactInfos.add( ai );
+                            page.add( convert( ai, null ) );
+                        } );
+            }
+            return new IndexerCoreSearchResponseImpl( searchRequest, 
totalHitsCount, page, query, artifactInfos );
+        }
+        else
+        {
+            GroupedSearchRequest groupedSearchRequest =
+                    new GroupedSearchRequest( query, new GAGrouping(), 
indexingContext );
+
+            try ( GroupedSearchResponse groupedSearchResponse = 
indexer.searchGrouped( groupedSearchRequest ) )
+            {
+                totalHitsCount = groupedSearchResponse.getResults().size();
+                groupedSearchResponse.getResults().values().stream()
+                        .skip( (long) paging.getPageSize() * 
paging.getPageOffset() ).limit( paging.getPageSize() )
+                        .forEach( aig ->
+                        {
+                            ArtifactInfo ai = 
aig.getArtifactInfos().iterator().next();
+                            artifactInfos.add( ai );
+                            page.add( convert( ai, 
aig.getArtifactInfos().size() ) );
+                        } );
+            }
+            return new IndexerCoreSearchResponseImpl( searchRequest, 
totalHitsCount, page, query, artifactInfos );
+        }
+    }
+
+    private Query toQuery( HashSet<Field> searchedFields, 
org.apache.maven.search.request.Query query )
+    {
+        if ( query instanceof org.apache.maven.search.request.BooleanQuery.And 
)
+        {
+            org.apache.maven.search.request.BooleanQuery bq =
+                    (org.apache.maven.search.request.BooleanQuery) query;
+            return new BooleanQuery.Builder().add(
+                            new BooleanClause( toQuery( searchedFields, 
bq.getLeft() ), BooleanClause.Occur.MUST ) )
+                    .add( new BooleanClause( toQuery( searchedFields, 
bq.getRight() ), BooleanClause.Occur.MUST ) )
+                    .build();
+        }
+        else if ( query instanceof FieldQuery )
+        {
+            FieldQuery fq =
+                    (FieldQuery) query;
+            org.apache.maven.index.Field icFieldName = FIELD_TRANSLATION.get( 
fq.getField() );
+            if ( icFieldName != null )
+            {
+                searchedFields.add( fq.getField() );
+                if ( fq.getValue().endsWith( "*" ) )
+                {
+                    return indexer.constructQuery( icFieldName, fq.getValue(), 
SearchType.SCORED );
+                }
+                else
+                {
+                    return indexer.constructQuery( icFieldName, fq.getValue(), 
SearchType.EXACT );
+                }
+            }
+            else
+            {
+                throw new IllegalArgumentException( "Unsupported Indexer 
field: " + fq.getField() );
+            }
+        }
+        return new BooleanQuery.Builder().add( new BooleanClause(
+                indexer.constructQuery( org.apache.maven.index.MAVEN.GROUP_ID, 
query.getValue(), SearchType.SCORED ),
+                BooleanClause.Occur.SHOULD ) ).add( new BooleanClause(
+                indexer.constructQuery( 
org.apache.maven.index.MAVEN.ARTIFACT_ID, query.getValue(), SearchType.SCORED ),
+                BooleanClause.Occur.SHOULD ) ).add( new BooleanClause(
+                indexer.constructQuery( org.apache.maven.index.MAVEN.NAME, 
query.getValue(), SearchType.SCORED ),
+                BooleanClause.Occur.SHOULD ) ).build();
+    }
+
+    private Record convert( ArtifactInfo ai, /* nullable */ Integer 
versionCount )
+    {
+        HashMap<Field, Object> result = new HashMap<>();
+
+        mayPut( result, MAVEN.GROUP_ID, ai.getGroupId() );
+        mayPut( result, MAVEN.ARTIFACT_ID, ai.getArtifactId() );
+        mayPut( result, MAVEN.VERSION, ai.getVersion() );
+        mayPut( result, MAVEN.PACKAGING, ai.getPackaging() );
+        mayPut( result, MAVEN.CLASSIFIER, ai.getClassifier() );
+        mayPut( result, MAVEN.FILE_EXTENSION, ai.getFileExtension() );
+
+        mayPut( result, MAVEN.VERSION_COUNT, versionCount );
+
+        mayPut( result, MAVEN.HAS_SOURCE, ai.getSourcesExists() == 
ArtifactAvailability.PRESENT );
+        mayPut( result, MAVEN.HAS_JAVADOC, ai.getJavadocExists() == 
ArtifactAvailability.PRESENT );
+        mayPut( result, MAVEN.HAS_GPG_SIGNATURE, ai.getSignatureExists() == 
ArtifactAvailability.PRESENT );
+
+        return new Record( getBackendId(), getRepositoryId(), ai.getUinfo(), 
ai.getLastModified(), result );
+    }
+
+    private static void mayPut( Map<Field, Object> result, Field fieldName, /* 
nullable */ Object value )
+    {
+        if ( value != null )
+        {
+            result.put( fieldName, value );
+        }
+    }
+}
diff --git 
a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java
 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java
new file mode 100644
index 0000000..d33206c
--- /dev/null
+++ 
b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java
@@ -0,0 +1,61 @@
+package org.apache.maven.search.backend.indexer.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.apache.lucene.search.Query;
+import org.apache.maven.index.ArtifactInfo;
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.backend.indexer.IndexerCoreSearchResponse;
+import org.apache.maven.search.support.SearchResponseSupport;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * An engine to perform search trough single repository index (endpoint).
+ */
+public class IndexerCoreSearchResponseImpl extends SearchResponseSupport 
implements IndexerCoreSearchResponse
+{
+    private final Query query;
+
+    private final List<ArtifactInfo> artifactInfos;
+
+    public IndexerCoreSearchResponseImpl( SearchRequest searchRequest, int 
totalHits, List<Record> page,
+                                          Query query, List<ArtifactInfo> 
artifactInfos )
+    {
+        super( searchRequest, totalHits, page );
+        this.query = requireNonNull( query );
+        this.artifactInfos = requireNonNull( artifactInfos );
+    }
+
+    @Override
+    public Query getQuery()
+    {
+        return query;
+    }
+
+    @Override
+    public List<ArtifactInfo> getArtifactInfos()
+    {
+        return artifactInfos;
+    }
+}
diff --git 
a/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java
 
b/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java
new file mode 100644
index 0000000..2250841
--- /dev/null
+++ 
b/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java
@@ -0,0 +1,283 @@
+package org.apache.maven.search.backend.indexer.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.index.Indexer;
+import org.apache.maven.index.context.IndexCreator;
+import org.apache.maven.index.context.IndexingContext;
+import org.apache.maven.search.MAVEN;
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.SearchResponse;
+import org.apache.maven.index.updater.IndexUpdateRequest;
+import org.apache.maven.index.updater.IndexUpdateResult;
+import org.apache.maven.index.updater.IndexUpdater;
+import org.apache.maven.index.updater.ResourceFetcher;
+import org.apache.maven.index.updater.WagonHelper;
+import org.apache.maven.search.request.FieldQuery;
+import org.apache.maven.wagon.Wagon;
+import org.apache.maven.wagon.events.TransferEvent;
+import org.apache.maven.wagon.events.TransferListener;
+import org.apache.maven.wagon.observers.AbstractTransferListener;
+import org.codehaus.plexus.DefaultContainerConfiguration;
+import org.codehaus.plexus.DefaultPlexusContainer;
+import org.codehaus.plexus.PlexusConstants;
+import org.codehaus.plexus.PlexusContainer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.maven.search.request.BooleanQuery.and;
+import static org.apache.maven.search.request.Query.query;
+
+@Ignore("This is more a showcase")
+public class IndexerCoreSearchBackendImplTest
+{
+    private PlexusContainer plexusContainer;
+
+    private Indexer indexer;
+
+    private IndexUpdater indexUpdater;
+
+    private Wagon httpWagon;
+
+    private IndexingContext centralContext;
+
+    private IndexerCoreSearchBackendImpl backend;
+
+    private void dumpSingle( AtomicInteger counter, List<Record> page )
+    {
+        for ( Record record : page )
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.append( record.getValue( MAVEN.GROUP_ID ) ).append( ":" 
).append( record.getValue( MAVEN.ARTIFACT_ID ) )
+                    .append( ":" ).append( record.getValue( MAVEN.VERSION ) );
+            if ( record.hasField( MAVEN.PACKAGING ) )
+            {
+                if ( record.hasField( MAVEN.CLASSIFIER ) )
+                {
+                    sb.append( ":" ).append( record.getValue( MAVEN.CLASSIFIER 
) );
+                }
+                sb.append( ":" ).append( record.getValue( MAVEN.PACKAGING ) );
+            }
+
+            List<String> remarks = new ArrayList<>();
+            if ( record.getLastUpdated() != null )
+            {
+                remarks.add( "lastUpdate=" + Instant.ofEpochMilli( 
record.getLastUpdated() ) );
+            }
+            if ( record.hasField( MAVEN.VERSION_COUNT ) )
+            {
+                remarks.add( "versionCount=" + record.getValue( 
MAVEN.VERSION_COUNT ) );
+            }
+            if ( record.hasField( MAVEN.HAS_SOURCE ) )
+            {
+                remarks.add( "hasSource=" + record.getValue( MAVEN.HAS_SOURCE 
) );
+            }
+            if ( record.hasField( MAVEN.HAS_JAVADOC ) )
+            {
+                remarks.add( "hasJavadoc=" + record.getValue( 
MAVEN.HAS_JAVADOC ) );
+            }
+
+            System.out.print( counter.incrementAndGet() + ". " + sb );
+            if ( !remarks.isEmpty() )
+            {
+                System.out.print( " " + remarks );
+            }
+            System.out.println();
+        }
+    }
+
+    private void dumpPage( SearchResponse searchResponse ) throws IOException
+    {
+        AtomicInteger counter = new AtomicInteger( 0 );
+        System.out.println("QUERY: " + 
searchResponse.getSearchRequest().getQuery().toString());
+        dumpSingle( counter, searchResponse.getPage() );
+        while ( searchResponse.getCurrentHits() > 0 )
+        {
+            searchResponse = backend.search( 
searchResponse.getSearchRequest().nextPage() );
+            dumpSingle( counter, searchResponse.getPage() );
+            if ( counter.get() > 50 )
+            {
+                System.out.println( "ABORTED TO NOT SPAM" );
+                break; // do not spam the SMO service
+            }
+        }
+        System.out.println();
+    }
+
+    @Before
+    public void prepareAndUpdateBackend() throws Exception
+    {
+        final DefaultContainerConfiguration config = new 
DefaultContainerConfiguration();
+        config.setClassPathScanning( PlexusConstants.SCANNING_CACHE );
+        this.plexusContainer = new DefaultPlexusContainer( config );
+
+        // lookup the indexer components from plexus
+        this.indexer = plexusContainer.lookup( Indexer.class );
+        this.indexUpdater = plexusContainer.lookup( IndexUpdater.class );
+        // lookup wagon used to remotely fetch index
+        this.httpWagon = plexusContainer.lookup( Wagon.class, "http" );
+
+        // Files where local cache is (if any) and Lucene Index should be 
located
+        File centralLocalCache = new File( "target/central-cache" );
+        File centralIndexDir = new File( "target/central-index" );
+
+        // Creators we want to use (search for fields it defines)
+        List<IndexCreator> indexers = new ArrayList<>();
+        indexers.add( plexusContainer.lookup( IndexCreator.class, "min" ) );
+        indexers.add( plexusContainer.lookup( IndexCreator.class, "jarContent" 
) );
+        indexers.add( plexusContainer.lookup( IndexCreator.class, 
"maven-plugin" ) );
+
+        // Create context for central repository index
+        centralContext = indexer.createIndexingContext( "central-context", 
"central", centralLocalCache,
+                centralIndexDir, "https://repo1.maven.org/maven2";, null, true, 
true, indexers );
+
+        // Update the index (incremental update will happen if this is not 1st 
run and files are not deleted)
+        // This whole block below should not be executed on every app start, 
but rather controlled by some configuration
+        // since this block will always emit at least one HTTP GET. Central 
indexes are updated once a week, but
+        // other index sources might have different index publishing frequency.
+        // Preferred frequency is once a week.
+        System.out.println( "Updating Index..." );
+        System.out.println( "This might take a while on first run, so please 
be patient!" );
+        // Create ResourceFetcher implementation to be used with 
IndexUpdateRequest
+        // Here, we use Wagon based one as shorthand, but all we need is a 
ResourceFetcher implementation
+        TransferListener listener = new AbstractTransferListener()
+        {
+            public void transferStarted( TransferEvent transferEvent )
+            {
+                System.out.print( "  Downloading " + 
transferEvent.getResource().getName() );
+            }
+
+            public void transferProgress( TransferEvent transferEvent, byte[] 
buffer, int length )
+            {
+            }
+
+            public void transferCompleted( TransferEvent transferEvent )
+            {
+                System.out.println( " - Done" );
+            }
+        };
+        ResourceFetcher resourceFetcher = new WagonHelper.WagonFetcher( 
httpWagon, listener, null, null );
+
+        Date centralContextCurrentTimestamp = centralContext.getTimestamp();
+        IndexUpdateRequest updateRequest = new IndexUpdateRequest( 
centralContext, resourceFetcher );
+        IndexUpdateResult updateResult = indexUpdater.fetchAndUpdateIndex( 
updateRequest );
+        if ( updateResult.isFullUpdate() )
+        {
+            System.out.println( "Full update happened!" );
+        }
+        else if ( updateResult.getTimestamp().equals( 
centralContextCurrentTimestamp ) )
+        {
+            System.out.println( "No update needed, index is up to date!" );
+        }
+        else
+        {
+            System.out.println(
+                    "Incremental update happened, change covered " + 
centralContextCurrentTimestamp + " - " + updateResult.getTimestamp() + " 
period." );
+        }
+        System.out.println();
+
+        this.backend = new IndexerCoreSearchBackendImpl( indexer, 
centralContext );
+    }
+
+    @After
+    public void cleanup() throws IOException
+    {
+        indexer.closeIndexingContext( centralContext, false );
+        plexusContainer.dispose();
+    }
+
+    @Test
+    public void smoke() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( query( "smoke" ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void g() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void ga() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( and( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ),
+                FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" 
) ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void gav() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( and( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ),
+                FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" 
), FieldQuery.fieldQuery( MAVEN.VERSION, "3.1.0" ) ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void sha1() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest(
+                FieldQuery.fieldQuery( MAVEN.SHA1, 
"8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12" ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void cn() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( 
FieldQuery.fieldQuery( MAVEN.CLASS_NAME, "MavenRepositorySystem" ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void fqcn() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest(
+                FieldQuery.fieldQuery( MAVEN.FQ_CLASS_NAME, 
"org.apache.maven.bridge.MavenRepositorySystem" ) );
+        SearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+}
diff --git a/search-backend-smo/README.md b/search-backend-smo/README.md
new file mode 100644
index 0000000..3431a0c
--- /dev/null
+++ b/search-backend-smo/README.md
@@ -0,0 +1,13 @@
+Indexer Search SMO Backend
+==========================
+
+Search API SMO (https://search.maven.org/) backend implementation.
+
+By default uses GSON only, so is Java8, Android and GraalVM (untested) 
friendly.
+The default transport is java.net.HttpUrlConnection, but is pluggable.
+
+Examples:
+
+```java
+  SmoSearchBackendImpl backend = new SmoSearchBackendImpl(); // creates 
default SMO backend
+```
diff --git a/search-backend-smo/header.txt b/search-backend-smo/header.txt
new file mode 100644
index 0000000..1a2ef73
--- /dev/null
+++ b/search-backend-smo/header.txt
@@ -0,0 +1,17 @@
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+
diff --git a/search-backend-smo/pom.xml b/search-backend-smo/pom.xml
new file mode 100644
index 0000000..5a282e2
--- /dev/null
+++ b/search-backend-smo/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.indexer</groupId>
+    <artifactId>maven-indexer</artifactId>
+    <version>6.1.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>search-backend-smo</artifactId>
+
+  <name>Maven :: Search API SMO Backend</name>
+  <description>
+    Indexer Search Backend implemented by SMO.
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.indexer</groupId>
+      <artifactId>search-api</artifactId>
+    </dependency>
+
+    <!-- SMO -->
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.9.0</version>
+    </dependency>
+
+    <!-- Test -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/filtered-resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git 
a/search-backend-smo/src/main/filtered-resources/smo-version.properties 
b/search-backend-smo/src/main/filtered-resources/smo-version.properties
new file mode 100644
index 0000000..62e70a1
--- /dev/null
+++ b/search-backend-smo/src/main/filtered-resources/smo-version.properties
@@ -0,0 +1,18 @@
+#Licensed to the Apache Software Foundation (ASF) under one
+#or more contributor license agreements.  See the NOTICE file
+#distributed with this work for additional information
+#regarding copyright ownership.  The ASF licenses this file
+#to you under the Apache License, Version 2.0 (the
+#"License"); you may not use this file except in compliance
+#with the License.  You may obtain a copy of the License at
+#
+#http://www.apache.org/licenses/LICENSE-2.0
+#
+#Unless required by applicable law or agreed to in writing,
+#software distributed under the License is distributed on an
+#"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#KIND, either express or implied.  See the License for the
+#specific language governing permissions and limitations
+#under the License.
+#
+version=${project.version}
\ No newline at end of file
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java
new file mode 100644
index 0000000..26b27cb
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java
@@ -0,0 +1,33 @@
+package org.apache.maven.search.backend.smo;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.search.SearchBackend;
+
+/**
+ * The SMO search backend.
+ */
+public interface SmoSearchBackend extends SearchBackend
+{
+    /**
+     * Returns the base "service URI" that is used by this SMO backend. never 
{@code null}.
+     */
+    String getSmoUri();
+}
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java
new file mode 100644
index 0000000..a89e7d2
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java
@@ -0,0 +1,38 @@
+package org.apache.maven.search.backend.smo;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.search.SearchResponse;
+
+/**
+ * The SMO search response.
+ */
+public interface SmoSearchResponse extends SearchResponse
+{
+    /**
+     * Returns the full search URI (base + params) that was used for this 
search, never {@code null}.
+     */
+    String getSearchUri();
+
+    /**
+     * Returns "raw" JSON response from SMO endpoint, never {@code null}.
+     */
+    String getRawJsonResponse();
+}
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java
new file mode 100644
index 0000000..f3df5e8
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java
@@ -0,0 +1,235 @@
+package org.apache.maven.search.backend.smo.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import org.apache.maven.search.MAVEN;
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.backend.smo.SmoSearchBackend;
+import org.apache.maven.search.backend.smo.SmoSearchResponse;
+import org.apache.maven.search.request.BooleanQuery;
+import org.apache.maven.search.request.Field;
+import org.apache.maven.search.request.FieldQuery;
+import org.apache.maven.search.request.Paging;
+import org.apache.maven.search.request.Query;
+import org.apache.maven.search.support.SearchBackendSupport;
+
+import static java.util.Objects.requireNonNull;
+
+public class SmoSearchBackendImpl extends SearchBackendSupport implements 
SmoSearchBackend
+{
+    public static final String DEFAULT_BACKEND_ID = "central-smo";
+
+    public static final String DEFAULT_REPOSITORY_ID = "central";
+
+    public static final String DEFAULT_SMO_URI = 
"https://search.maven.org/solrsearch/select";;
+
+    private static final Map<Field, String> FIELD_TRANSLATION;
+
+    static
+    {
+        HashMap<Field, String> map = new HashMap<>();
+        map.put( MAVEN.GROUP_ID, "g" );
+        map.put( MAVEN.ARTIFACT_ID, "a" );
+        map.put( MAVEN.VERSION, "v" );
+        map.put( MAVEN.CLASSIFIER, "l" );
+        map.put( MAVEN.PACKAGING, "p" );
+        map.put( MAVEN.CLASS_NAME, "c" );
+        map.put( MAVEN.FQ_CLASS_NAME, "fc" );
+        map.put( MAVEN.SHA1, "1" );
+        FIELD_TRANSLATION = Collections.unmodifiableMap( map );
+    }
+
+    private final String smoUri;
+
+    private final SmoSearchTransportSupport transportSupport;
+
+    /**
+     * Creates a "default" instance of SMO backend against {@link 
#DEFAULT_SMO_URI}.
+     */
+    public SmoSearchBackendImpl()
+    {
+        this( DEFAULT_BACKEND_ID, DEFAULT_REPOSITORY_ID, DEFAULT_SMO_URI, new 
UrlConnectionSmoSearchTransport() );
+    }
+
+    /**
+     * Creates a customized instance of SMO backend, like an in-house 
instances of SMO or different IDs.
+     */
+    public SmoSearchBackendImpl( String backendId, String repositoryId, String 
smoUri,
+                                 SmoSearchTransportSupport transportSupport )
+    {
+        super( backendId, repositoryId );
+        this.smoUri = requireNonNull( smoUri );
+        this.transportSupport = requireNonNull( transportSupport );
+    }
+
+    @Override
+    public String getSmoUri()
+    {
+        return smoUri;
+    }
+
+    @Override
+    public SmoSearchResponse search( SearchRequest searchRequest ) throws 
IOException
+    {
+        String searchUri = toURI( searchRequest );
+        String payload = transportSupport.fetch( searchRequest, searchUri );
+        JsonObject raw = JsonParser.parseString( payload ).getAsJsonObject();
+        List<Record> page = new ArrayList<>( 
searchRequest.getPaging().getPageSize() );
+        int totalHits = populateFromRaw( raw, page );
+        return new SmoSearchResponseImpl( searchRequest, totalHits, page, 
searchUri, payload );
+    }
+
+    private String toURI( SearchRequest searchRequest ) throws 
UnsupportedEncodingException
+    {
+        Paging paging = searchRequest.getPaging();
+        HashSet<Field> searchedFields = new HashSet<>();
+        String smoQuery = toSMOQuery( searchedFields, searchRequest.getQuery() 
);
+        smoQuery += "&start=" + paging.getPageSize() * paging.getPageOffset();
+        smoQuery += "&rows=" + paging.getPageSize();
+        smoQuery += "&wt=json";
+        if ( searchedFields.contains( MAVEN.GROUP_ID ) && 
searchedFields.contains( MAVEN.ARTIFACT_ID ) )
+        {
+            smoQuery += "&core=gav";
+        }
+        return smoUri + "?q=" + smoQuery;
+    }
+
+    private String toSMOQuery( HashSet<Field> searchedFields, Query query ) 
throws UnsupportedEncodingException
+    {
+        if ( query instanceof BooleanQuery.And )
+        {
+            BooleanQuery bq = (BooleanQuery) query;
+            return toSMOQuery( searchedFields, bq.getLeft() ) + "%20AND%20"
+                    + toSMOQuery( searchedFields, bq.getRight() );
+        }
+        else if ( query instanceof FieldQuery )
+        {
+            FieldQuery fq = (FieldQuery) query;
+            String smoFieldName = FIELD_TRANSLATION.get( fq.getField() );
+            if ( smoFieldName != null )
+            {
+                searchedFields.add( fq.getField() );
+                return smoFieldName + ":" + encodeQueryParameterValue( 
fq.getValue() );
+            }
+            else
+            {
+                throw new IllegalArgumentException( "Unsupported SMO field: " 
+ fq.getField() );
+            }
+        }
+        return encodeQueryParameterValue( query.getValue() );
+    }
+
+    private String encodeQueryParameterValue( String parameterValue ) throws 
UnsupportedEncodingException
+    {
+        return URLEncoder.encode( parameterValue, 
StandardCharsets.UTF_8.name() )
+                .replace( "+", "%20" );
+    }
+
+    private int populateFromRaw( JsonObject raw, List<Record> page )
+    {
+        JsonObject response = raw.getAsJsonObject( "response" );
+        Number numFound = response.get( "numFound" ).getAsNumber();
+
+        JsonArray docs = response.getAsJsonArray( "docs" );
+        for ( JsonElement doc : docs )
+        {
+            page.add( convert( (JsonObject) doc ) );
+        }
+        return numFound.intValue();
+    }
+
+    private Record convert( JsonObject doc )
+    {
+        HashMap<Field, Object> result = new HashMap<>();
+
+        mayPut( result, MAVEN.GROUP_ID, mayGet( "g", doc ) );
+        mayPut( result, MAVEN.ARTIFACT_ID, mayGet( "a", doc ) );
+        String version = mayGet( "v", doc );
+        if ( version == null )
+        {
+            version = mayGet( "latestVersion", doc );
+        }
+        mayPut( result, MAVEN.VERSION, version );
+        mayPut( result, MAVEN.PACKAGING, mayGet( "p", doc ) );
+        mayPut( result, MAVEN.CLASSIFIER, mayGet( "l", doc ) );
+
+        // version count
+        Number versionCount = doc.has( "versionCount" ) ? doc.get( 
"versionCount" ).getAsNumber() : null;
+        if ( versionCount != null )
+        {
+            mayPut( result, MAVEN.VERSION_COUNT, versionCount.intValue() );
+        }
+        // ec
+        JsonArray ec = doc.getAsJsonArray( "ec" );
+        if ( ec != null )
+        {
+            result.put( MAVEN.HAS_SOURCE, ec.contains( EC_SOURCE_JAR ) );
+            result.put( MAVEN.HAS_JAVADOC, ec.contains( EC_JAVADOC_JAR ) );
+            // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) 
);
+        }
+
+        return new Record(
+                getBackendId(),
+                getRepositoryId(),
+                doc.has( "id" ) ? doc.get( "id" ).getAsString() : null,
+                doc.has( "timestamp" ) ? doc.get( "timestamp" ).getAsLong() : 
null,
+                result
+        );
+    }
+
+    private static final JsonPrimitive EC_SOURCE_JAR = new JsonPrimitive( 
"-sources.jar" );
+
+    private static final JsonPrimitive EC_JAVADOC_JAR = new JsonPrimitive( 
"-javadoc.jar" );
+
+    private static String mayGet( String field, JsonObject object )
+    {
+        return object.has( field ) ? object.get( field ).getAsString() : null;
+    }
+
+    private static void mayPut( Map<Field, Object> result, Field fieldName, 
Object value )
+    {
+        if ( value == null )
+        {
+            return;
+        }
+        if ( value instanceof String && ( (String) value ).trim().isEmpty() )
+        {
+            return;
+        }
+        result.put( fieldName, value );
+    }
+}
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java
new file mode 100644
index 0000000..3cb6f87
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java
@@ -0,0 +1,56 @@
+package org.apache.maven.search.backend.smo.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.backend.smo.SmoSearchResponse;
+import org.apache.maven.search.support.SearchResponseSupport;
+
+import static java.util.Objects.requireNonNull;
+
+public class SmoSearchResponseImpl extends SearchResponseSupport implements 
SmoSearchResponse
+{
+    private final String searchUri;
+
+    private final String rawJsonResponse;
+
+    public SmoSearchResponseImpl( SearchRequest searchRequest, int totalHits, 
List<Record> page,
+                                  String searchUri, String rawJsonResponse )
+    {
+        super( searchRequest, totalHits, page );
+        this.searchUri = requireNonNull( searchUri );
+        this.rawJsonResponse = requireNonNull( rawJsonResponse );
+    }
+
+    @Override
+    public String getSearchUri()
+    {
+        return searchUri;
+    }
+
+    @Override
+    public String getRawJsonResponse()
+    {
+        return rawJsonResponse;
+    }
+}
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java
new file mode 100644
index 0000000..07c5402
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java
@@ -0,0 +1,81 @@
+package org.apache.maven.search.backend.smo.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import org.apache.maven.search.SearchRequest;
+
+/**
+ * A trivial "transport abstraction" to make possible pluggable 
implementations.
+ */
+public abstract class SmoSearchTransportSupport
+{
+    private final String clientVersion;
+
+    public SmoSearchTransportSupport()
+    {
+        this.clientVersion = discoverVersion();
+    }
+
+    private String discoverVersion()
+    {
+        Properties properties = new Properties();
+        InputStream inputStream = 
getClass().getClassLoader().getResourceAsStream( "smo-version.properties" );
+        if ( inputStream != null )
+        {
+            try ( InputStream is = inputStream )
+            {
+                properties.load( is );
+            }
+            catch ( IOException e )
+            {
+                // fall through
+            }
+        }
+        return properties.getProperty( "version", "unknown" );
+    }
+
+    /**
+     * Exposes this backend version, for example to be used in HTTP {@code 
User-Agent} string, never {@code null}.
+     */
+    protected String getClientVersion()
+    {
+        return clientVersion;
+    }
+
+    /**
+     * Exposes full HTTP {@code User-Agent} string ready to be used by HTTP 
clients, never {@code null}.
+     */
+    protected String getUserAgent()
+    {
+        return "Apache Search SMO/" + getClientVersion();
+    }
+
+    /**
+     * This method should issue a HTTP GET requests using {@code serviceUri} 
and return body payload as {@link String}
+     * ONLY if the response was HTTP 200 Ok and there was a payload returned 
by service. In any other case, it should
+     * throw, never return {@code null}. The payload is expected to be {@code 
application/json}, so client may add
+     * headers to request. Also, the payload is expected to be "relatively 
small".
+     */
+    public abstract String fetch( SearchRequest searchRequest, String 
serviceUri ) throws IOException;
+}
diff --git 
a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java
 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java
new file mode 100644
index 0000000..a40a78f
--- /dev/null
+++ 
b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java
@@ -0,0 +1,59 @@
+package org.apache.maven.search.backend.smo.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+import org.apache.maven.search.SearchRequest;
+
+/**
+ * {@link java.net.HttpURLConnection} backed transport.
+ */
+public class UrlConnectionSmoSearchTransport extends SmoSearchTransportSupport
+{
+    @Override
+    public String fetch( SearchRequest searchRequest, String serviceUri ) 
throws IOException
+    {
+        HttpURLConnection httpConnection = (HttpURLConnection) new URL( 
serviceUri ).openConnection();
+        httpConnection.setInstanceFollowRedirects( false );
+        httpConnection.setRequestProperty( "User-Agent", getUserAgent() );
+        httpConnection.setRequestProperty( "Accept", "application/json" );
+        int httpCode = httpConnection.getResponseCode();
+        if ( httpCode == HttpURLConnection.HTTP_OK )
+        {
+            try ( InputStream inputStream = httpConnection.getInputStream() )
+            {
+                try ( Scanner scanner = new Scanner( inputStream, 
StandardCharsets.UTF_8.name() ) )
+                {
+                    return scanner.useDelimiter( "\\A" ).next();
+                }
+            }
+        }
+        else
+        {
+            throw new IOException( "Unexpected response code: " + httpCode );
+        }
+    }
+}
diff --git 
a/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java
 
b/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java
new file mode 100644
index 0000000..4b29f15
--- /dev/null
+++ 
b/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java
@@ -0,0 +1,172 @@
+package org.apache.maven.search.backend.smo.internal;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.maven.search.MAVEN;
+import org.apache.maven.search.Record;
+import org.apache.maven.search.SearchRequest;
+import org.apache.maven.search.backend.smo.SmoSearchResponse;
+import org.apache.maven.search.request.BooleanQuery;
+import org.apache.maven.search.request.FieldQuery;
+import org.apache.maven.search.request.Query;
+import org.junit.Ignore;
+import org.junit.Test;
+
+@Ignore( "This is more a showcase" )
+public class SmoSearchBackendImplTest
+{
+    private final SmoSearchBackendImpl backend = new SmoSearchBackendImpl();
+
+    private void dumpSingle( AtomicInteger counter, List<Record> page )
+    {
+        for ( Record record : page )
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.append( record.getValue( MAVEN.GROUP_ID ) ).append( ":" 
).append( record.getValue( MAVEN.ARTIFACT_ID ) )
+                    .append( ":" ).append( record.getValue( MAVEN.VERSION ) );
+            if ( record.hasField( MAVEN.PACKAGING ) )
+            {
+                if ( record.hasField( MAVEN.CLASSIFIER ) )
+                {
+                    sb.append( ":" ).append( record.getValue( MAVEN.CLASSIFIER 
) );
+                }
+                sb.append( ":" ).append( record.getValue( MAVEN.PACKAGING ) );
+            }
+
+            List<String> remarks = new ArrayList<>();
+            if ( record.getLastUpdated() != null )
+            {
+                remarks.add( "lastUpdate=" + Instant.ofEpochMilli( 
record.getLastUpdated() ) );
+            }
+            if ( record.hasField( MAVEN.VERSION_COUNT ) )
+            {
+                remarks.add( "versionCount=" + record.getValue( 
MAVEN.VERSION_COUNT ) );
+            }
+            if ( record.hasField( MAVEN.HAS_SOURCE ) )
+            {
+                remarks.add( "hasSource=" + record.getValue( MAVEN.HAS_SOURCE 
) );
+            }
+            if ( record.hasField( MAVEN.HAS_JAVADOC ) )
+            {
+                remarks.add( "hasJavadoc=" + record.getValue( 
MAVEN.HAS_JAVADOC ) );
+            }
+
+            System.out.print( counter.incrementAndGet() + ". " + sb );
+            if ( !remarks.isEmpty() )
+            {
+                System.out.print( " " + remarks );
+            }
+            System.out.println();
+        }
+    }
+
+    private void dumpPage( SmoSearchResponse searchResponse ) throws 
IOException
+    {
+        AtomicInteger counter = new AtomicInteger( 0 );
+        System.out.println( "QUERY: " + 
searchResponse.getSearchRequest().getQuery().toString() );
+        System.out.println( "URL: " + searchResponse.getSearchUri() );
+        dumpSingle( counter, searchResponse.getPage() );
+        while ( searchResponse.getTotalHits() > 
searchResponse.getCurrentHits() )
+        {
+            System.out.println( "NEXT PAGE (size " + 
searchResponse.getSearchRequest().getPaging().getPageSize() + ")" );
+            searchResponse = backend.search( 
searchResponse.getSearchRequest().nextPage() );
+            dumpSingle( counter, searchResponse.getPage() );
+            if ( counter.get() > 50 )
+            {
+                System.out.println( "ABORTED TO NOT SPAM" );
+                break; // do not spam the SMO service
+            }
+        }
+        System.out.println();
+    }
+
+    @Test
+    public void smoke() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( Query.query( "smoke" 
) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void g() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void ga() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( BooleanQuery.and( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ),
+                FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" 
) ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void gav() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( BooleanQuery.and( 
FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ),
+                FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" 
), FieldQuery.fieldQuery( MAVEN.VERSION, "3.1.0" ) ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void sha1() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest(
+                FieldQuery.fieldQuery( MAVEN.SHA1, 
"8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12" ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void cn() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest( 
FieldQuery.fieldQuery( MAVEN.CLASS_NAME, "MavenRepositorySystem" ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+
+    @Test
+    public void fqcn() throws IOException
+    {
+        SearchRequest searchRequest = new SearchRequest(
+                FieldQuery.fieldQuery( MAVEN.FQ_CLASS_NAME, 
"org.apache.maven.bridge.MavenRepositorySystem" ) );
+        SmoSearchResponse searchResponse = backend.search( searchRequest );
+        System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() );
+        dumpPage( searchResponse );
+    }
+}

Reply via email to