diff options
author | Jean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com> | 2013-11-06 16:28:03 +0100 |
---|---|---|
committer | Jean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com> | 2013-11-06 16:28:03 +0100 |
commit | 8aabceea511700fb8ff95be315fdc86411b4d4fe (patch) | |
tree | 38d06ff5d67bf2eb37b4976b651ab748a89ffdd8 | |
parent | a6830c9a8aa83306a28d2a7fdf67d7415a08bf51 (diff) | |
download | sonarqube-8aabceea511700fb8ff95be315fdc86411b4d4fe.tar.gz sonarqube-8aabceea511700fb8ff95be315fdc86411b4d4fe.zip |
SONAR-4832 Integrate ElasticSearch into sonar-server (with initial WIP API)
11 files changed, 586 insertions, 1 deletions
@@ -79,6 +79,7 @@ <logback.version>1.0.13</logback.version> <slf4j.version>1.7.5</slf4j.version> <tomcat.version>7.0.42</tomcat.version> + <elasticsearch.version>0.90.5</elasticsearch.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.min.version>3.0.5</maven.min.version> <maven.api.version>2.2.0</maven.api.version> diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index 63a9423e04a..27799dba52c 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -282,7 +282,7 @@ <rules> <requireFilesSize> <minsize>55000000</minsize> - <maxsize>60400000</maxsize> + <maxsize>75000000</maxsize> <files> <file>${project.build.directory}/sonarqube-${project.version}.zip</file> </files> diff --git a/sonar-server/pom.xml b/sonar-server/pom.xml index 293c7e7ab6b..ed96eff0aa2 100644 --- a/sonar-server/pom.xml +++ b/sonar-server/pom.xml @@ -126,6 +126,11 @@ <artifactId>jruby-rack</artifactId> </dependency> <dependency> + <groupId>org.elasticsearch</groupId> + <artifactId>elasticsearch</artifactId> + <version>${elasticsearch.version}</version> + </dependency> + <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>7.0.42</version> diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 5af173d2839..83da0f3c6c1 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -93,6 +93,8 @@ import org.sonar.server.plugins.*; import org.sonar.server.rule.RubyRuleService; import org.sonar.server.rules.ProfilesConsole; import org.sonar.server.rules.RulesConsole; +import org.sonar.server.search.SearchIndex; +import org.sonar.server.search.SearchNode; import org.sonar.server.startup.*; import org.sonar.server.technicaldebt.InternalRubyTechnicalDebtService; import org.sonar.server.technicaldebt.TechnicalDebtFormatter; @@ -210,6 +212,7 @@ public final class Platform { coreContainer.addSingleton(ThreadLocalDatabaseSessionFactory.class); coreContainer.addPicoAdapter(new DatabaseSessionProvider()); coreContainer.addSingleton(ServerMetadataPersister.class); + coreContainer.addSingleton(SearchNode.class); coreContainer.startComponents(); } @@ -219,6 +222,7 @@ public final class Platform { private void startServiceComponents() { servicesContainer = coreContainer.createChild(); + servicesContainer.addSingleton(SearchIndex.class); servicesContainer.addSingleton(HttpDownloader.class); servicesContainer.addSingleton(UriReader.class); servicesContainer.addSingleton(UpdateCenterClient.class); diff --git a/sonar-server/src/main/java/org/sonar/server/search/SearchIndex.java b/sonar-server/src/main/java/org/sonar/server/search/SearchIndex.java new file mode 100644 index 00000000000..ad8c9ee0120 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/search/SearchIndex.java @@ -0,0 +1,120 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.search; + +import org.apache.commons.io.IOUtils; +import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequestBuilder; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.common.io.BytesStream; +import org.elasticsearch.index.query.QueryBuilders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +public class SearchIndex { + + private static final Logger LOG = LoggerFactory.getLogger(SearchIndex.class); + + private SearchNode searchNode; + private Client client; + + public SearchIndex(SearchNode searchNode) { + this.searchNode = searchNode; + } + + public void start() { + this.client = searchNode.client(); + } + + public void stop() { + if(client != null) { + client.close(); + } + } + + public void put(String index, String type, String id, BytesStream source) { + client.prepareIndex(index, type, id).setSource(source.bytes()).execute(); + } + + public void put(String index, String type, String id, BytesStream source, String parent) { + client.prepareIndex(index, type, id).setParent(parent).setSource(source.bytes()).execute(); + } + + public void bulkIndex(String index, String type, String[] ids, BytesStream[] sources) { + BulkRequestBuilder builder = new BulkRequestBuilder(client); + for (int i=0; i<ids.length; i++) { + builder.add(client.prepareIndex(index, type, ids[i]).setSource(sources[i].bytes())); + } + try { + BulkResponse bulkResponse = client.bulk(builder.setRefresh(true).request()).get(); + if (bulkResponse.hasFailures()) { + // Retry once per failed doc -- ugly + for (BulkItemResponse bulkItemResponse : bulkResponse.getItems()) { + if(bulkItemResponse.isFailed()) { + int itemId = bulkItemResponse.getItemId(); + put(index, type, ids[itemId], sources[itemId]); + } + } + } + } catch (InterruptedException e) { + LOG.error("Interrupted during bulk operation", e); + } catch (ExecutionException e) { + LOG.error("Execution of bulk operation failed", e); + } + } + + public void addMappingFromClasspath(String index, String type, String resourcePath) { + try { + addMapping(index, type, IOUtils.toString(getClass().getResource(resourcePath))); + } catch(IOException ioException) { + throw new IllegalStateException("Could not find mapping in classpath at "+resourcePath, ioException); + } + } + + private void addMapping(String index, String type, String mapping) { + IndicesAdminClient indices = client.admin().indices(); + try { + if (! indices.exists(new IndicesExistsRequest(index)).get().isExists()) { + indices.prepareCreate(index).get(); + } + } catch (Exception e) { + LOG.error("While checking for index existence", e); + } + indices.preparePutMapping(index).setType(type).setSource(mapping).execute(); + } + + public void stats(String index) { + LOG.info( + String.format( + "Index %s contains %d elements", index, + client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).get().getHits().totalHits())); + } + + public SearchResponse find(SearchQuery query) { + return query.toBuilder(client).get(); + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/search/SearchNode.java b/sonar-server/src/main/java/org/sonar/server/search/SearchNode.java new file mode 100644 index 00000000000..7bae90ca352 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/search/SearchNode.java @@ -0,0 +1,108 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.search; + +import com.google.common.annotations.VisibleForTesting; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.node.Node; +import org.elasticsearch.node.NodeBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.TempFolder; + +/** + * Manages the ElasticSearch Node instance used to connect to the index. + * @since 4.1 + */ +public class SearchNode { + + private static final Logger LOG = LoggerFactory.getLogger(SearchIndex.class); + + private String nodeDir; + private Settings settings; + + private ImmutableSettings.Builder nodeSettingsBuilder; + private NodeBuilder nodeBuilder; + + private Node node; + + public SearchNode(TempFolder tempFolder, Settings settings) { + this(tempFolder, settings, ImmutableSettings.builder(), NodeBuilder.nodeBuilder()); + } + + @VisibleForTesting + SearchNode(TempFolder tempFolder, Settings settings, ImmutableSettings.Builder nodeSettingsBuilder, NodeBuilder nodeBuilder) { + this.nodeDir = tempFolder.newDir("es").getAbsolutePath(); + this.settings = settings; + this.nodeSettingsBuilder = nodeSettingsBuilder; + this.nodeBuilder = nodeBuilder; + } + + public void start() { + LOG.info("Starting {} in {}", this.getClass().getSimpleName(), nodeDir); + nodeSettingsBuilder + .put("node.path.conf", nodeDir) + .put("node.path.data", nodeDir) + .put("node.path.work", nodeDir) + .put("node.path.logs", nodeDir) + .put("gateway.type", "none") + .put("index.store.type", "ram") + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 0); + + String httpHost = settings.getString("sonar.es.http.host"); + String httpPort = settings.getString("sonar.es.http.port"); + if (httpPort == null) { + LOG.info("HTTP access to search cache disabled"); + nodeSettingsBuilder.put("http.enabled", false); + } else { + if (httpHost == null) { + httpHost = "127.0.0.1"; + } + LOG.info("Enabling HTTP access to search cache on ports {}", httpPort); + nodeSettingsBuilder.put("http.enabled", true); + nodeSettingsBuilder.put("http.host", httpHost); + nodeSettingsBuilder.put("http.port", httpPort); + } + + node = nodeBuilder + .local(true) + .clusterName("sonarqube") + .settings(nodeSettingsBuilder) + .node(); + } + + public void stop() { + if(node != null) { + node.close(); + node = null; + } + } + + public Client client() { + if (node == null) { + throw new IllegalStateException(this.getClass().getSimpleName() + " not started"); + } + return node.client(); + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/search/SearchQuery.java b/sonar-server/src/main/java/org/sonar/server/search/SearchQuery.java new file mode 100644 index 00000000000..74976f59263 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/search/SearchQuery.java @@ -0,0 +1,95 @@ +package org.sonar.server.search; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.commons.lang.StringUtils; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.client.Client; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.facet.FacetBuilders; + +import java.util.List; +import java.util.Map; + +/** + * This class can be used to build "AND" form queries, eventually with query facets, to be passed to {@link SearchIndex#find(SearchQuery)} + * For instance the following code: + * <blockquote> + SearchQuery.create("polop") + .field("field1", "value1") + .field("field2", "value2") + </blockquote> + * ...yields the following query string:<br/> + * <blockquote> + polop AND field1:value1 AND field2:value2 + </blockquote> + * @since 4.1 + */ +public class SearchQuery { + private String searchString; + private List<String> indices; + private List<String> types; + private Map<String, String> fieldCriteria; + private Map<String, String> termFacets; + + private SearchQuery() { + indices = Lists.newArrayList(); + types = Lists.newArrayList(); + fieldCriteria = Maps.newLinkedHashMap(); + termFacets = Maps.newHashMap(); + } + + public static SearchQuery create() { + return new SearchQuery(); + } + + public static SearchQuery create(String searchString) { + SearchQuery searchQuery = new SearchQuery(); + searchQuery.searchString = searchString; + return searchQuery; + } + + public SearchQuery index(String index) { + indices.add(index); + return this; + } + + public SearchQuery field(String fieldName, String fieldValue) { + fieldCriteria.put(fieldName, fieldValue); + return this; + } + + public SearchQuery facet(String facetName, String fieldName) { + fieldCriteria.put(facetName, fieldName); + return this; + } + + private SearchRequestBuilder addFacets(SearchRequestBuilder builder) { + for (String facetName: termFacets.keySet()) { + builder.addFacet(FacetBuilders.termsFacet(facetName).field(termFacets.get(facetName))); + } + return builder; + } + + String getQueryString() { + List<String> criteria = Lists.newArrayList(); + if (StringUtils.isNotBlank(searchString)) { + criteria.add(searchString); + } + for (String fieldName: fieldCriteria.keySet()) { + criteria.add(String.format("%s:%s", fieldName, fieldCriteria.get(fieldName))); + } + return StringUtils.join(criteria, " AND "); + } + + SearchRequestBuilder toBuilder(Client client) { + SearchRequestBuilder builder = client.prepareSearch(indices.toArray(new String[0])).setTypes(types.toArray(new String[0])); + String queryString = getQueryString(); + if (StringUtils.isBlank(queryString)) { + builder.setQuery(QueryBuilders.matchAllQuery()); + } else { + builder.setQuery(queryString); + } + return addFacets(builder); + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/search/package-info.java b/sonar-server/src/main/java/org/sonar/server/search/package-info.java new file mode 100644 index 00000000000..2f141ea121d --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/search/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.search; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-server/src/test/java/org/sonar/server/search/SearchIndexTest.java b/sonar-server/src/test/java/org/sonar/server/search/SearchIndexTest.java new file mode 100644 index 00000000000..f10047b8ea9 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/search/SearchIndexTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.search; + +import org.elasticsearch.client.Client; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SearchIndexTest { + + private SearchNode searchNode; + + private Client client; + + private SearchIndex searchIndex; + + @Before + public void setUp() { + searchNode = mock(SearchNode.class); + client = mock(Client.class); + when(searchNode.client()).thenReturn(client); + + searchIndex = new SearchIndex(searchNode); + } + + @Test + public void should_start_and_stop_properly() { + searchIndex.start(); + + verify(searchNode).client(); + + searchIndex.stop(); + + verify(client).close(); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/search/SearchNodeTest.java b/sonar-server/src/test/java/org/sonar/server/search/SearchNodeTest.java new file mode 100644 index 00000000000..d68eb691e4a --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/search/SearchNodeTest.java @@ -0,0 +1,117 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.search; + +import org.elasticsearch.client.Client; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.node.Node; +import org.elasticsearch.node.NodeBuilder; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.TempFolder; + +import java.io.File; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SearchNodeTest { + + private TempFolder tempFolder; + private Settings settings; + private ImmutableSettings.Builder settingsBuilder; + private NodeBuilder nodeBuilder; + private SearchNode searchNode; + + @Before + public void createMocks() { + tempFolder = mock(TempFolder.class); + when(tempFolder.newDir("es")).thenReturn(mock(File.class)); + settings = mock(Settings.class); + + settingsBuilder = mock(ImmutableSettings.Builder.class); + when(settingsBuilder.put(anyString(), anyString())).thenReturn(settingsBuilder); + when(settingsBuilder.put(anyString(), anyInt())).thenReturn(settingsBuilder); + + nodeBuilder = mock(NodeBuilder.class); + when(nodeBuilder.local(anyBoolean())).thenReturn(nodeBuilder); + when(nodeBuilder.clusterName(anyString())).thenReturn(nodeBuilder); + when(nodeBuilder.settings(settingsBuilder)).thenReturn(nodeBuilder); + + searchNode = new SearchNode(tempFolder, settings, settingsBuilder, nodeBuilder); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_if_not_properly_started() { + searchNode.stop(); + searchNode.client(); + } + + @Test + public void should_manage_node_without_http() { + Node node = mock(Node.class); + Client client = mock(Client.class); + + when(nodeBuilder.node()).thenReturn(node); + when(node.client()).thenReturn(client); + + searchNode.start(); + assertThat(searchNode.client()).isEqualTo(client); + searchNode.stop(); + + verify(settingsBuilder).put("http.enabled", false); + verify(nodeBuilder).node(); + verify(node).client(); + verify(node).close(); + } + + @Test + public void should_initialize_node_with_http() { + String httpHost = "httpHost"; + String httpPort = "httpPort"; + when(settings.getString("sonar.es.http.host")).thenReturn(httpHost); + when(settings.getString("sonar.es.http.port")).thenReturn(httpPort); + + searchNode.start(); + + verify(settingsBuilder).put("http.enabled", true); + verify(settingsBuilder).put("http.host", httpHost); + verify(settingsBuilder).put("http.port", httpPort); + } + + @Test + public void should_initialize_node_with_http_on_localhost() { + String httpPort = "httpPort"; + when(settings.getString("sonar.es.http.port")).thenReturn(httpPort); + + searchNode.start(); + + verify(settingsBuilder).put("http.enabled", true); + verify(settingsBuilder).put("http.host", "127.0.0.1"); + verify(settingsBuilder).put("http.port", httpPort); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/search/SearchQueryTest.java b/sonar-server/src/test/java/org/sonar/server/search/SearchQueryTest.java new file mode 100644 index 00000000000..57266eafb09 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/search/SearchQueryTest.java @@ -0,0 +1,54 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.search; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class SearchQueryTest { + + @Test + public void should_return_empty_query() { + assertThat(SearchQuery.create().getQueryString()).isEmpty(); + } + + @Test + public void should_handle_custom_query() { + assertThat(SearchQuery.create("polop").getQueryString()).isEqualTo("polop"); + } + + @Test + public void should_add_fields() { + assertThat(SearchQuery.create() + .field("field1", "value1") + .field("field2", "value2") + .getQueryString()).isEqualTo("field1:value1 AND field2:value2"); + } + + @Test + public void should_add_fields_to_custom_query() { + assertThat(SearchQuery.create("polop") + .field("field1", "value1") + .field("field2", "value2") + .getQueryString()).isEqualTo("polop AND field1:value1 AND field2:value2"); + } + +} |