Преглед на файлове

refactored http handlers; implemented distributed updates and deletes for json queries

Ilya Kuznetsov преди 8 години
родител
ревизия
f1ea5bc18b

+ 4 - 4
docs/http_reference/json_bulk.rst

@@ -9,8 +9,8 @@ Example:
 
 
 ::
 ::
 
 
-	{ "insert" : { "_index" : "test", "_id" : 1, "doc": { "gid" : 10, "content" : "doc one" } } }
-	{ "insert" : { "_index" : "test", "_id" : 2, "doc": { "gid" : 20, "content" : "doc two" } } }
+	{ "insert" : { "index" : "test", "id" : 1, "doc": { "gid" : 10, "content" : "doc one" } } }
+	{ "insert" : { "index" : "test", "id" : 2, "doc": { "gid" : 20, "content" : "doc two" } } }
 
 
 This inserts two documents to index ``test``. Each statement starts with an action type (in this case, ``insert``). Here's a list of the supported actions:
 This inserts two documents to index ``test``. Each statement starts with an action type (in this case, ``insert``). Here's a list of the supported actions:
 
 
@@ -27,7 +27,7 @@ Example:
 
 
 ::
 ::
 
 
-	{ "update" : { "_index" : "test", "doc": { "tag" : 1000 }, "query": { "range": { "price": { "gte": 1000 } } } } }
-	{ "delete" : { "_index" : "test", "query": { "range": { "price": { "lt": 1000 } } } } }
+	{ "update" : { "index" : "test", "doc": { "tag" : 1000 }, "query": { "range": { "price": { "gte": 1000 } } } } }
+	{ "delete" : { "index" : "test", "query": { "range": { "price": { "lt": 1000 } } } } }
 
 
 Note that the bulk operation stops at the first query that results in an error.
 Note that the bulk operation stops at the first query that results in an error.

+ 3 - 3
docs/http_reference/json_delete.rst

@@ -10,8 +10,8 @@ Example:
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
-	  "_id":1
+	  "index":"test",
+	  "id":1
 	}
 	}
 
 
 	
 	
@@ -33,7 +33,7 @@ As in ``json/update``, you can do a delete by query.
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
+	  "index":"test",
 
 
 	  "query":
 	  "query":
   	  {
   	  {

+ 7 - 7
docs/http_reference/json_insert.rst

@@ -10,17 +10,17 @@ Here's how you can index a simple document:
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
-	  "_id":1
+	  "index":"test",
+	  "id":1
 	}
 	}
 
 
-This creates a document with an id specified by ``_id`` in an index specified by the ``_index`` property. This document has empty fulltext fields and all attributes are set to their default values. However, you can use the optional ``doc`` property to set field and attribute values:
+This creates a document with an id specified by ``id`` in an index specified by the ``index`` property. This document has empty fulltext fields and all attributes are set to their default values. However, you can use the optional ``doc`` property to set field and attribute values:
 
 
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
-	  "_id":1,
+	  "index":"test",
+	  "id":1,
 	  "doc":
 	  "doc":
 	  {
 	  {
 	    "gid" : 10,
 	    "gid" : 10,
@@ -45,8 +45,8 @@ MVA attributes are inserted as arrays of numbers. JSON attributes can be inserte
 ::
 ::
 
 
   {
   {
-    "_index":"test",
-    "_id":1,
+    "index":"test",
+    "id":1,
     "doc":
     "doc":
     {
     {
       "mva" : [1,2,3,4,5],
       "mva" : [1,2,3,4,5],

+ 2 - 2
docs/http_reference/json_replace.rst

@@ -8,8 +8,8 @@ json/replace
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
-	  "_id":1,
+	  "index":"test",
+	  "id":1,
 	  "doc":
 	  "doc":
 	  {
 	  {
 	    "gid" : 10,
 	    "gid" : 10,

+ 5 - 5
docs/http_reference/json_update.rst

@@ -10,8 +10,8 @@ Example:
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
-	  "_id":1,
+	  "index":"test",
+	  "id":1,
 	  "doc":
 	  "doc":
 	  {
 	  {
 	    "gid" : 100,
 	    "gid" : 100,
@@ -29,13 +29,13 @@ The daemon will respond with a JSON object stating if the operation was successf
     "result": "updated"
     "result": "updated"
   }	
   }	
   
   
-The id of the document that needs to be updated can be set directly using the ``_id`` property (as in the example above) or
+The id of the document that needs to be updated can be set directly using the ``id`` property (as in the example above) or
 you can do an update by query and apply the update to all the documents that match the query:
 you can do an update by query and apply the update to all the documents that match the query:
 
 
 ::
 ::
 
 
 	{
 	{
-	  "_index":"test",
+	  "index":"test",
 	  "doc":
 	  "doc":
 	  {
 	  {
 	    "price" : 1000
 	    "price" : 1000
@@ -47,5 +47,5 @@ you can do an update by query and apply the update to all the documents that mat
 	  }
 	  }
 	}
 	}
 
 
-Query syntax is the same as in the ``json/search`` endpoint. Note that you can't specify ``_id`` and ``query`` at the same time.
+Query syntax is the same as in the ``json/search`` endpoint. Note that you can't specify ``id`` and ``query`` at the same time.
 
 

+ 87 - 36
src/searchd.cpp

@@ -415,7 +415,7 @@ enum
 /// command names
 /// command names
 static const char * g_dApiCommands[SEARCHD_COMMAND_TOTAL] =
 static const char * g_dApiCommands[SEARCHD_COMMAND_TOTAL] =
 {
 {
-	"search", "excerpt", "update", "keywords", "persist", "status", "query", "flushattrs", "query", "ping", "delete", "set",  "insert", "replace", "commit", "suggest"
+	"search", "excerpt", "update", "keywords", "persist", "status", "query", "flushattrs", "query", "ping", "delete", "set",  "insert", "replace", "commit", "suggest", "json"
 };
 };
 
 
 STATIC_ASSERT ( sizeof(g_dApiCommands)/sizeof(g_dApiCommands[0])==SEARCHD_COMMAND_TOTAL, SEARCHD_COMMAND_SHOULD_BE_SAME_AS_SEARCHD_COMMAND_TOTAL );
 STATIC_ASSERT ( sizeof(g_dApiCommands)/sizeof(g_dApiCommands[0])==SEARCHD_COMMAND_TOTAL, SEARCHD_COMMAND_SHOULD_BE_SAME_AS_SEARCHD_COMMAND_TOTAL );
@@ -6682,7 +6682,7 @@ protected:
 	bool							m_bMultiQueue;	///< whether current subset is subject to multi-queue optimization
 	bool							m_bMultiQueue;	///< whether current subset is subject to multi-queue optimization
 	bool							m_bFacetQueue;	///< whether current subset is subject to facet-queue optimization
 	bool							m_bFacetQueue;	///< whether current subset is subject to facet-queue optimization
 	CSphVector<LocalIndex_t>		m_dLocal;		///< local indexes for the current subset
 	CSphVector<LocalIndex_t>		m_dLocal;		///< local indexes for the current subset
-	mutable CSphVector<CSphSchemaMT>		m_dExtraSchemas; ///< the extra fields for agents
+	mutable CSphVector<CSphSchemaMT> m_dExtraSchemas; ///< the extra fields for agents
 	CSphAttrUpdateEx *				m_pUpdates;		///< holder for updates
 	CSphAttrUpdateEx *				m_pUpdates;		///< holder for updates
 	CSphVector<SphDocID_t> *		m_pDelete;		///< this query is for deleting
 	CSphVector<SphDocID_t> *		m_pDelete;		///< this query is for deleting
 
 
@@ -10964,7 +10964,7 @@ void UpdateRequestBuilder_t::BuildRequest ( const AgentConn_t & tAgent, ISphOutp
 	bool bMva = false;
 	bool bMva = false;
 	ARRAY_FOREACH ( i, m_tUpd.m_dTypes )
 	ARRAY_FOREACH ( i, m_tUpd.m_dTypes )
 	{
 	{
-		assert ( m_tUpd.m_dTypes[i]!=SPH_ATTR_INT64SET ); // mva64 goes only via SphinxQL (SphinxqlRequestBuilder_t)
+		assert ( m_tUpd.m_dTypes[i]!=SPH_ATTR_INT64SET ); // mva64 goes only via SphinxQL (SphinxqlRequestBuilder_c)
 		bMva |= ( m_tUpd.m_dTypes[i]==SPH_ATTR_UINT32SET );
 		bMva |= ( m_tUpd.m_dTypes[i]==SPH_ATTR_UINT32SET );
 	}
 	}
 
 
@@ -11024,7 +11024,7 @@ void UpdateRequestBuilder_t::BuildRequest ( const AgentConn_t & tAgent, ISphOutp
 		// size down in case of MVA
 		// size down in case of MVA
 		// MVA stored as mva64 in pool but API could handle only mva32 due to HandleCommandUpdate
 		// MVA stored as mva64 in pool but API could handle only mva32 due to HandleCommandUpdate
 		// SphinxQL only could work either mva32 or mva64 and only SphinxQL could receive mva64 updates
 		// SphinxQL only could work either mva32 or mva64 and only SphinxQL could receive mva64 updates
-		// SphinxQL master communicate to agent via SphinxqlRequestBuilder_t
+		// SphinxQL master communicate to agent via SphinxqlRequestBuilder_c
 
 
 		ARRAY_FOREACH ( iDoc, m_tUpd.m_dDocids )
 		ARRAY_FOREACH ( iDoc, m_tUpd.m_dDocids )
 		{
 		{
@@ -11439,6 +11439,8 @@ void BuildStatus ( VectorLike & dStatus )
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iCommandCount[SEARCHD_COMMAND_COMMIT] );
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iCommandCount[SEARCHD_COMMAND_COMMIT] );
 	if ( dStatus.MatchAdd ( "command_suggest" ) )
 	if ( dStatus.MatchAdd ( "command_suggest" ) )
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iCommandCount[SEARCHD_COMMAND_SUGGEST] );
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iCommandCount[SEARCHD_COMMAND_SUGGEST] );
+	if ( dStatus.MatchAdd ( "command_json" ) )
+		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iCommandCount[SEARCHD_COMMAND_JSON] );
 	if ( dStatus.MatchAdd ( "agent_connect" ) )
 	if ( dStatus.MatchAdd ( "agent_connect" ) )
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iAgentConnect );
 		dStatus.Add().SetSprintf ( FMT64, (int64_t) g_tStats.m_iAgentConnect );
 	if ( dStatus.MatchAdd ( "agent_retry" ) )
 	if ( dStatus.MatchAdd ( "agent_retry" ) )
@@ -11935,6 +11937,7 @@ void HandleCommandFlush ( ISphOutputBuffer & tOut, WORD uVer )
 }
 }
 
 
 void HandleCommandSphinxql ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq, ThdDesc_t * pThd ); // definition is below
 void HandleCommandSphinxql ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq, ThdDesc_t * pThd ); // definition is below
+void HandleCommandJson ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq, ThdDesc_t * pThd );
 void StatCountCommand ( SearchdCommand_e eCmd );
 void StatCountCommand ( SearchdCommand_e eCmd );
 void HandleCommandUserVar ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq );
 void HandleCommandUserVar ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq );
 
 
@@ -12145,6 +12148,7 @@ bool LoopClientSphinx ( SearchdCommand_e eCommand, WORD uCommandVer, int iLength
 		case SEARCHD_COMMAND_STATUS:	HandleCommandStatus ( tOut, uCommandVer, tBuf ); break;
 		case SEARCHD_COMMAND_STATUS:	HandleCommandStatus ( tOut, uCommandVer, tBuf ); break;
 		case SEARCHD_COMMAND_FLUSHATTRS:HandleCommandFlush ( tOut, uCommandVer ); break;
 		case SEARCHD_COMMAND_FLUSHATTRS:HandleCommandFlush ( tOut, uCommandVer ); break;
 		case SEARCHD_COMMAND_SPHINXQL:	HandleCommandSphinxql ( tOut, uCommandVer, tBuf, pThd ); break;
 		case SEARCHD_COMMAND_SPHINXQL:	HandleCommandSphinxql ( tOut, uCommandVer, tBuf, pThd ); break;
+		case SEARCHD_COMMAND_JSON:		HandleCommandJson ( tOut, uCommandVer, tBuf, pThd ); break;
 		case SEARCHD_COMMAND_PING:		HandleCommandPing ( tOut, uCommandVer, tBuf ); break;
 		case SEARCHD_COMMAND_PING:		HandleCommandPing ( tOut, uCommandVer, tBuf ); break;
 		case SEARCHD_COMMAND_UVAR:		HandleCommandUserVar ( tOut, uCommandVer, tBuf ); break;
 		case SEARCHD_COMMAND_UVAR:		HandleCommandUserVar ( tOut, uCommandVer, tBuf ); break;
 		default:						assert ( 0 && "INTERNAL ERROR: unhandled command" ); break;
 		default:						assert ( 0 && "INTERNAL ERROR: unhandled command" ); break;
@@ -13273,13 +13277,15 @@ void HandleMysqlPercolateMeta ( const PercolateMatchResult_t & tMeta, SqlRowBuff
 }
 }
 
 
 
 
-class PlainParserFactory_c : public QueryParserFactory_i
+class SphinxqlRequestBuilder_c : public IRequestBuilder_t
 {
 {
 public:
 public:
-	QueryParser_i * Create () const override
-	{
-		return sphCreatePlainQueryParser();
-	}
+			SphinxqlRequestBuilder_c ( const CSphString & sQuery, const SqlStmt_t & tStmt );
+	void	BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const override;
+
+protected:
+	const CSphString m_sBegin;
+	const CSphString m_sEnd;
 };
 };
 
 
 
 
@@ -15092,21 +15098,6 @@ void HandleCommandUserVar ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c &
 // SMART UPDATES HANDLER
 // SMART UPDATES HANDLER
 /////////////////////////////////////////////////////////////////////////////
 /////////////////////////////////////////////////////////////////////////////
 
 
-struct SphinxqlRequestBuilder_t : public IRequestBuilder_t
-{
-	explicit SphinxqlRequestBuilder_t ( const CSphString& sQuery, const SqlStmt_t & tStmt )
-		: m_sBegin ( sQuery.cstr(), tStmt.m_iListStart )
-		, m_sEnd ( sQuery.cstr() + tStmt.m_iListEnd, sQuery.Length() - tStmt.m_iListEnd )
-	{
-	}
-	virtual void BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const;
-
-protected:
-	const CSphString m_sBegin;
-	const CSphString m_sEnd;
-};
-
-
 struct SphinxqlReplyParser_t : public IReplyParser_t
 struct SphinxqlReplyParser_t : public IReplyParser_t
 {
 {
 	explicit SphinxqlReplyParser_t ( int * pUpd, int * pWarns )
 	explicit SphinxqlReplyParser_t ( int * pUpd, int * pWarns )
@@ -15147,7 +15138,14 @@ protected:
 };
 };
 
 
 
 
-void SphinxqlRequestBuilder_t::BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const
+SphinxqlRequestBuilder_c::SphinxqlRequestBuilder_c ( const CSphString& sQuery, const SqlStmt_t & tStmt )
+	: m_sBegin ( sQuery.cstr(), tStmt.m_iListStart )
+	, m_sEnd ( sQuery.cstr() + tStmt.m_iListEnd, sQuery.Length() - tStmt.m_iListEnd )
+{
+}
+
+
+void SphinxqlRequestBuilder_c::BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const
 {
 {
 	const char* sIndexes = tAgent.m_tDesc.m_sIndexes.cstr();
 	const char* sIndexes = tAgent.m_tDesc.m_sIndexes.cstr();
 	int iReqSize = strlen(sIndexes) + m_sBegin.Length() + m_sEnd.Length(); // indexes string
 	int iReqSize = strlen(sIndexes) + m_sBegin.Length() + m_sEnd.Length(); // indexes string
@@ -15163,6 +15161,26 @@ void SphinxqlRequestBuilder_t::BuildRequest ( const AgentConn_t & tAgent, ISphOu
 	tOut.SendBytes ( m_sEnd.cstr(), m_sEnd.Length() );
 	tOut.SendBytes ( m_sEnd.cstr(), m_sEnd.Length() );
 }
 }
 
 
+
+class PlainParserFactory_c : public QueryParserFactory_i
+{
+public:
+	QueryParser_i * CreateQueryParser () const override
+	{
+		return sphCreatePlainQueryParser();
+	}
+
+	IRequestBuilder_t * CreateRequestBuilder ( const CSphString & sQuery, const SqlStmt_t & tStmt ) const override
+	{
+		return new SphinxqlRequestBuilder_c ( sQuery, tStmt );
+	}
+
+	IReplyParser_t * CreateReplyParser ( int & iUpdated, int & iWarnings ) const override
+	{
+		return new SphinxqlReplyParser_t ( &iUpdated, &iWarnings );
+	}
+};
+
 //////////////////////////////////////////////////////////////////////////
 //////////////////////////////////////////////////////////////////////////
 
 
 static void DoExtendedUpdate ( const char * sIndex, const QueryParserFactory_i & tQueryParserFactory, const char * sDistributed, const SqlStmt_t & tStmt, int & iSuccesses, int & iUpdated,
 static void DoExtendedUpdate ( const char * sIndex, const QueryParserFactory_i & tQueryParserFactory, const char * sDistributed, const SqlStmt_t & tStmt, int & iSuccesses, int & iUpdated,
@@ -15176,7 +15194,7 @@ static void DoExtendedUpdate ( const char * sIndex, const QueryParserFactory_i &
 		return;
 		return;
 	}
 	}
 
 
-	SearchHandler_c tHandler ( 1, tQueryParserFactory.Create(), tStmt.m_tQuery.m_eQueryType, false, iCID ); // handler unlocks index at destructor - no need to do it manually
+	SearchHandler_c tHandler ( 1, tQueryParserFactory.CreateQueryParser(), tStmt.m_tQuery.m_eQueryType, false, iCID ); // handler unlocks index at destructor - no need to do it manually
 	CSphAttrUpdateEx tUpdate;
 	CSphAttrUpdateEx tUpdate;
 	CSphString sError;
 	CSphString sError;
 
 
@@ -15267,13 +15285,13 @@ void sphHandleMysqlUpdate ( StmtErrorReporter_i & tOut, const QueryParserFactory
 			pDist->GetAllAgents ( dAgents );
 			pDist->GetAllAgents ( dAgents );
 
 
 			// connect to remote agents and query them
 			// connect to remote agents and query them
-			SphinxqlRequestBuilder_t tReqBuilder ( sQuery, tStmt );
-			CSphScopedPtr<ISphRemoteAgentsController> pDistCtrl ( GetAgentsController ( g_iDistThreads, dAgents, tReqBuilder, pDist->m_iAgentConnectTimeout ) );
+			CSphScopedPtr<IRequestBuilder_t> pRequestBuilder ( tQueryParserFactory.CreateRequestBuilder ( sQuery, tStmt ) ) ;
+			CSphScopedPtr<ISphRemoteAgentsController> pDistCtrl ( GetAgentsController ( g_iDistThreads, dAgents, *pRequestBuilder.Ptr(), pDist->m_iAgentConnectTimeout ) );
 			int iAgentsDone = pDistCtrl->Finish();
 			int iAgentsDone = pDistCtrl->Finish();
 			if ( iAgentsDone )
 			if ( iAgentsDone )
 			{
 			{
-				SphinxqlReplyParser_t tParser ( &iUpdated, &iWarns );
-				iSuccesses += RemoteWaitForAgents ( dAgents, pDist->m_iAgentQueryTimeout, tParser ); // FIXME? profile update time too?
+				CSphScopedPtr<IReplyParser_t> pReplyParser ( tQueryParserFactory.CreateReplyParser ( iUpdated, iWarns ) );
+				iSuccesses += RemoteWaitForAgents ( dAgents, pDist->m_iAgentQueryTimeout, *pReplyParser.Ptr() ); // FIXME? profile update time too?
 			}
 			}
 		}
 		}
 	}
 	}
@@ -15903,7 +15921,7 @@ static int LocalIndexDoDeleteDocuments ( const char * sName, const QueryParserFa
 	CSphVector<SphDocID_t> dValues;
 	CSphVector<SphDocID_t> dValues;
 	if ( !pDocs ) // needs to be deleted via query
 	if ( !pDocs ) // needs to be deleted via query
 	{
 	{
-		pHandler = new SearchHandler_c ( 1, tQueryParserFactory.Create(), tStmt.m_tQuery.m_eQueryType, false, iCID ); // handler unlocks index at destructor - no need to do it manually
+		pHandler = new SearchHandler_c ( 1, tQueryParserFactory.CreateQueryParser(), tStmt.m_tQuery.m_eQueryType, false, iCID ); // handler unlocks index at destructor - no need to do it manually
 		pHandler->RunDeletes ( tStmt.m_tQuery, sName, pLocked, &sError, &dValues );
 		pHandler->RunDeletes ( tStmt.m_tQuery, sName, pLocked, &sError, &dValues );
 		pDocs = dValues.Begin();
 		pDocs = dValues.Begin();
 		iCount = dValues.GetLength();
 		iCount = dValues.GetLength();
@@ -16029,16 +16047,16 @@ void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const QueryParserFactory
 			pDist->GetAllAgents ( dAgents );
 			pDist->GetAllAgents ( dAgents );
 
 
 			// connect to remote agents and query them
 			// connect to remote agents and query them
-			SphinxqlRequestBuilder_t tReqBuilder ( sQuery, tStmt );
-			CSphScopedPtr<ISphRemoteAgentsController> pDistCtrl ( GetAgentsController ( g_iDistThreads, dAgents, tReqBuilder, pDist->m_iAgentConnectTimeout ) );
+			CSphScopedPtr<IRequestBuilder_t> pRequestBuilder ( tQueryParserFactory.CreateRequestBuilder ( sQuery, tStmt ) ) ;
+			CSphScopedPtr<ISphRemoteAgentsController> pDistCtrl ( GetAgentsController ( g_iDistThreads, dAgents, *pRequestBuilder.Ptr(), pDist->m_iAgentConnectTimeout ) );
 			int iAgentsDone = pDistCtrl->Finish();
 			int iAgentsDone = pDistCtrl->Finish();
 			if ( iAgentsDone )
 			if ( iAgentsDone )
 			{
 			{
 				// FIXME!!! report error & warnings from agents
 				// FIXME!!! report error & warnings from agents
 				int iGot = 0;
 				int iGot = 0;
 				int iWarns = 0;
 				int iWarns = 0;
-				SphinxqlReplyParser_t tParser ( &iGot, &iWarns );
-				RemoteWaitForAgents ( dAgents, pDist->m_iAgentQueryTimeout, tParser ); // FIXME? profile update time too?
+				CSphScopedPtr<IReplyParser_t> pReplyParser ( tQueryParserFactory.CreateReplyParser ( iGot, iWarns ) );
+				RemoteWaitForAgents ( dAgents, pDist->m_iAgentQueryTimeout, *pReplyParser.Ptr() ); // FIXME? profile update time too?
 				iAffected += iGot;
 				iAffected += iGot;
 			}
 			}
 		}
 		}
@@ -17887,6 +17905,35 @@ void HandleCommandSphinxql ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c &
 	tOut.Flush();
 	tOut.Flush();
 }
 }
 
 
+/// json command over API
+void HandleCommandJson ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & tReq, ThdDesc_t * pThd )
+{
+	if ( !CheckCommandVersion ( uVer, VER_COMMAND_JSON, tOut ) )
+		return;
+
+	// parse request
+	CSphString sEndpoint = tReq.GetString ();
+	CSphString sCommand = tReq.GetString ();
+	
+	ESphHttpEndpoint eEndpoint = sphStrToHttpEndpoint ( sEndpoint );
+	assert ( eEndpoint!=SPH_HTTP_ENDPOINT_TOTAL );
+
+	CSphVector<BYTE> dResult;
+	SmallStringHash_T<CSphString> tOptions;
+	sphProcessHttpQuery ( eEndpoint, sCommand, tOptions, pThd->m_iConnID, dResult );
+
+	tOut.Flush();
+	tOut.SendWord ( SEARCHD_OK );
+	tOut.SendWord ( VER_COMMAND_JSON );
+	tOut.SendDword( dResult.GetLength()+sEndpoint.Length()+8 );
+
+	tOut.SendString ( sEndpoint.cstr() );
+	tOut.SendDword( dResult.GetLength() );
+	tOut.SendBytes ( dResult.Begin(), dResult.GetLength() );
+
+	tOut.Flush();
+}
+
 
 
 void StatCountCommand ( SearchdCommand_e eCmd )
 void StatCountCommand ( SearchdCommand_e eCmd )
 {
 {
@@ -23030,7 +23077,11 @@ void ThdJobHttp_t::Call ()
 		sphHttpErrorReply ( m_tState->m_dBuf, SPH_HTTP_STATUS_503, "server is in maintenance mode" );
 		sphHttpErrorReply ( m_tState->m_dBuf, SPH_HTTP_STATUS_503, "server is in maintenance mode" );
 		m_tState->m_bKeepSocket = false;
 		m_tState->m_bKeepSocket = false;
 	} else
 	} else
-		m_tState->m_bKeepSocket = sphLoopClientHttp ( m_tState->m_dBuf, m_tState->m_iConnID );
+	{
+		CSphVector<BYTE> dResult;
+		m_tState->m_bKeepSocket = sphLoopClientHttp ( m_tState->m_dBuf.Begin(), m_tState->m_dBuf.GetLength(), dResult, m_tState->m_iConnID );
+		m_tState->m_dBuf = std::move(dResult);
+	}
 
 
 	ThreadRemove ( &tThdDesc );
 	ThreadRemove ( &tThdDesc );
 
 

+ 30 - 3
src/searchdaemon.h

@@ -129,6 +129,7 @@ enum SearchdCommand_e : WORD
 	SEARCHD_COMMAND_REPLACE		= 13,
 	SEARCHD_COMMAND_REPLACE		= 13,
 	SEARCHD_COMMAND_COMMIT		= 14,
 	SEARCHD_COMMAND_COMMIT		= 14,
 	SEARCHD_COMMAND_SUGGEST		= 15,
 	SEARCHD_COMMAND_SUGGEST		= 15,
+	SEARCHD_COMMAND_JSON		= 16,
 
 
 	SEARCHD_COMMAND_TOTAL,
 	SEARCHD_COMMAND_TOTAL,
 	SEARCHD_COMMAND_WRONG = SEARCHD_COMMAND_TOTAL,
 	SEARCHD_COMMAND_WRONG = SEARCHD_COMMAND_TOTAL,
@@ -145,6 +146,7 @@ enum SearchdCommandV_e : WORD
 	VER_COMMAND_STATUS		= 0x101,
 	VER_COMMAND_STATUS		= 0x101,
 	VER_COMMAND_FLUSHATTRS	= 0x100,
 	VER_COMMAND_FLUSHATTRS	= 0x100,
 	VER_COMMAND_SPHINXQL	= 0x100,
 	VER_COMMAND_SPHINXQL	= 0x100,
+	VER_COMMAND_JSON		= 0x100,
 	VER_COMMAND_PING		= 0x100,
 	VER_COMMAND_PING		= 0x100,
 	VER_COMMAND_UVAR		= 0x100,
 	VER_COMMAND_UVAR		= 0x100,
 
 
@@ -893,10 +895,15 @@ public:
 };
 };
 
 
 
 
+struct IRequestBuilder_t;
+struct IReplyParser_t;
+
 class QueryParserFactory_i
 class QueryParserFactory_i
 {
 {
 public:
 public:
-	virtual QueryParser_i * Create() const = 0;
+	virtual QueryParser_i *		CreateQueryParser() const = 0;
+	virtual IRequestBuilder_t *	CreateRequestBuilder ( const CSphString & sQuery, const SqlStmt_t & tStmt ) const = 0;
+	virtual IReplyParser_t *	CreateReplyParser ( int & iUpdated, int & iWarnings ) const = 0;
 };
 };
 
 
 
 
@@ -912,17 +919,37 @@ enum ESphHttpStatus
 	SPH_HTTP_STATUS_TOTAL
 	SPH_HTTP_STATUS_TOTAL
 };
 };
 
 
+enum ESphHttpEndpoint
+{
+	SPH_HTTP_ENDPOINT_INDEX,
+	SPH_HTTP_ENDPOINT_SEARCH,
+	SPH_HTTP_ENDPOINT_SQL,
+	SPH_HTTP_ENDPOINT_JSON_SEARCH,
+	SPH_HTTP_ENDPOINT_JSON_INDEX,
+	SPH_HTTP_ENDPOINT_JSON_CREATE,
+	SPH_HTTP_ENDPOINT_JSON_INSERT,
+	SPH_HTTP_ENDPOINT_JSON_REPLACE,
+	SPH_HTTP_ENDPOINT_JSON_UPDATE,
+	SPH_HTTP_ENDPOINT_JSON_DELETE,
+	SPH_HTTP_ENDPOINT_JSON_BULK,
+
+	SPH_HTTP_ENDPOINT_TOTAL
+};
 
 
 bool CheckCommandVersion ( WORD uVer, WORD uDaemonVersion, ISphOutputBuffer & tOut );
 bool CheckCommandVersion ( WORD uVer, WORD uDaemonVersion, ISphOutputBuffer & tOut );
 ISphSearchHandler * sphCreateSearchHandler ( int iQueries, const QueryParser_i * pQueryParser, QueryType_e eQueryType, bool bMaster, int iCID );
 ISphSearchHandler * sphCreateSearchHandler ( int iQueries, const QueryParser_i * pQueryParser, QueryType_e eQueryType, bool bMaster, int iCID );
 void sphFormatFactors ( CSphVector<BYTE> & dOut, const unsigned int * pFactors, bool bJson );
 void sphFormatFactors ( CSphVector<BYTE> & dOut, const unsigned int * pFactors, bool bJson );
-bool sphLoopClientHttp ( CSphVector<BYTE> & dData, int iCID );
-void sphHttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * szError );
 bool sphParseSqlQuery ( const char * sQuery, int iLen, CSphVector<SqlStmt_t> & dStmt, CSphString & sError, ESphCollation eCollation );
 bool sphParseSqlQuery ( const char * sQuery, int iLen, CSphVector<SqlStmt_t> & dStmt, CSphString & sError, ESphCollation eCollation );
 void sphHandleMysqlInsert ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, bool bReplace, bool bCommit, CSphString & sWarning, CSphSessionAccum & tAcc, ESphCollation	eCollation );
 void sphHandleMysqlInsert ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, bool bReplace, bool bCommit, CSphString & sWarning, CSphSessionAccum & tAcc, ESphCollation	eCollation );
 void sphHandleMysqlUpdate ( StmtErrorReporter_i & tOut, const QueryParserFactory_i & tQueryParserFactory, const SqlStmt_t & tStmt, const CSphString & sQuery, CSphString & sWarning, int iCID );
 void sphHandleMysqlUpdate ( StmtErrorReporter_i & tOut, const QueryParserFactory_i & tQueryParserFactory, const SqlStmt_t & tStmt, const CSphString & sQuery, CSphString & sWarning, int iCID );
 void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const QueryParserFactory_i & tQueryParserFactory, const SqlStmt_t & tStmt, const CSphString & sQuery, bool bCommit, CSphSessionAccum & tAcc, int iCID );
 void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const QueryParserFactory_i & tQueryParserFactory, const SqlStmt_t & tStmt, const CSphString & sQuery, bool bCommit, CSphSessionAccum & tAcc, int iCID );
 
 
+bool				sphLoopClientHttp ( const BYTE * pRequest, int iRequestLen, CSphVector<BYTE> & dResult, int iCID );
+bool				sphProcessHttpQuery ( ESphHttpEndpoint eEndpoint, const CSphString & sQuery, const SmallStringHash_T<CSphString> & tOptions, int iCID, CSphVector<BYTE> & dResult, bool bNeedHttpResponse=false );
+void				sphHttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * szError );
+ESphHttpEndpoint	sphStrToHttpEndpoint ( const CSphString & sEndpoint );
+CSphString			sphHttpEndpointToStr ( ESphHttpEndpoint eEndpoint );
+
 // get tokens from sphinxql
 // get tokens from sphinxql
 int sphGetTokTypeInt();
 int sphGetTokTypeInt();
 int sphGetTokTypeFloat();
 int sphGetTokTypeFloat();

+ 630 - 402
src/searchdhttp.cpp

@@ -21,6 +21,7 @@
 #include "http/http_parser.h"
 #include "http/http_parser.h"
 #include "json/cJSON.h"
 #include "json/cJSON.h"
 #include "searchdaemon.h"
 #include "searchdaemon.h"
+#include "searchdha.h"
 
 
 struct EscapeJsonString_t
 struct EscapeJsonString_t
 {
 {
@@ -258,102 +259,67 @@ static void HttpBuildReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, con
 	memcpy ( dData.Begin() + iHeaderLen, sBody, iBodyLen );
 	memcpy ( dData.Begin() + iHeaderLen, sBody, iBodyLen );
 }
 }
 
 
-static void HttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * sTemplate, ... )
+
+static void HttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * szError )
 {
 {
 	cJSON * pError = cJSON_CreateObject();
 	cJSON * pError = cJSON_CreateObject();
 	assert ( pError );
 	assert ( pError );
 
 
-	char sErr[1008] = "\0";
-	if ( sTemplate && *sTemplate!='\0' )
-	{
-		va_list ap;
-		va_start ( ap, sTemplate );
-		vsnprintf ( sErr, sizeof(sErr), sTemplate, ap );
-		va_end ( ap );
-		
-		cJSON_AddStringToObject ( pError, "error", sErr );
-	}
-
-	CSphString sJsonError;
-	sJsonError.Adopt ( cJSON_Print ( pError ) );
+	cJSON_AddStringToObject ( pError, "error", szError );
+	CSphString sJsonError = sphJsonToString ( pError );
 	cJSON_Delete ( pError );
 	cJSON_Delete ( pError );
 
 
 	HttpBuildReply ( dData, eCode, sJsonError.cstr(), sJsonError.Length(), false );
 	HttpBuildReply ( dData, eCode, sJsonError.cstr(), sJsonError.Length(), false );
 }
 }
 
 
-enum ESphHttpEndpoint
-{
-	SPH_HTTP_ENDPOINT_INDEX,
-	SPH_HTTP_ENDPOINT_SEARCH,
-	SPH_HTTP_ENDPOINT_SQL,
-	SPH_HTTP_ENDPOINT_JSON_SEARCH,
-	SPH_HTTP_ENDPOINT_JSON_INDEX,
-	SPH_HTTP_ENDPOINT_JSON_CREATE,
-	SPH_HTTP_ENDPOINT_JSON_INSERT,
-	SPH_HTTP_ENDPOINT_JSON_REPLACE,
-	SPH_HTTP_ENDPOINT_JSON_UPDATE,
-	SPH_HTTP_ENDPOINT_JSON_DELETE,
-	SPH_HTTP_ENDPOINT_JSON_BULK,
-
-	SPH_HTTP_ENDPOINT_TOTAL,
-
-	SPH_HTTP_ENDPOINT_MISSED,
-	SPH_HTTP_ENDPOINT_DEFAULT = SPH_HTTP_ENDPOINT_MISSED
-};
 
 
-const char * g_sEndpoints[] = { "index.html", "search", "sql", "json/search", "json/index", "json/create", "json/insert", "json/replace", "json/update", "json/delete", "json/bulk" };
-STATIC_ASSERT ( sizeof(g_sEndpoints)/sizeof(g_sEndpoints[0])==SPH_HTTP_ENDPOINT_TOTAL, SPH_HTTP_ENDPOINT_SHOULD_BE_SAME_AS_SPH_HTTP_ENDPOINT_TOTAL );
+using OptionsHash_t = SmallStringHash_T<CSphString>;
 
 
-
-struct HttpRequestParser_t : public ISphNoncopyable
+class HttpRequestParser_c : public ISphNoncopyable
 {
 {
-	SmallStringHash_T<CSphString>	m_hOptions;
-	CSphString						m_sRawBody;
-
-	ESphHttpEndpoint				m_eEndpoint;
-	bool							m_bKeepAlive;
-
-	CSphString						m_sCurField;
-
-	const char *					m_sError;
-	CSphString						m_sInvalidEndpoint;
+public:
+	bool					Parse ( const BYTE * pData, int iDataLen );
+	bool					ParseList ( const char * sAt, int iLen );
 
 
-	HttpRequestParser_t ();
-	~HttpRequestParser_t () {}
+	const CSphString &		GetBody() const { return m_sRawBody; }
+	ESphHttpEndpoint		GetEndpoint() const { return m_eEndpoint; }
+	const OptionsHash_t &	GetOptions() const { return m_hOptions; }
+	const CSphString &		GetInvalidEndpoint() const { return m_sInvalidEndpoint; }
+	const char *			GetError() const { return m_szError; }
+	bool					GetKeepAlive() const { return m_bKeepAlive; }
 
 
-	bool Parse ( const CSphVector<BYTE> & dData );
-	bool ParseList ( const char * sAt, int iLen );
+	static int				ParserUrl ( http_parser * pParser, const char * sAt, size_t iLen );
+	static int				ParserHeaderField ( http_parser * pParser, const char * sAt, size_t iLen );
+	static int				ParserHeaderValue ( http_parser * pParser, const char * sAt, size_t iLen );
+	static int				ParserBody ( http_parser * pParser, const char * sAt, size_t iLen );
 
 
-	static int ParserUrl ( http_parser * pParser, const char * sAt, size_t iLen );
-	static int ParserHeaderField ( http_parser * pParser, const char * sAt, size_t iLen );
-	static int ParserHeaderValue ( http_parser * pParser, const char * sAt, size_t iLen );
-	static int ParserBody ( http_parser * pParser, const char * sAt, size_t iLen );
+private:
+	bool					m_bKeepAlive {false};
+	const char *			m_szError {nullptr};
+	ESphHttpEndpoint		m_eEndpoint {SPH_HTTP_ENDPOINT_TOTAL};
+	CSphString				m_sInvalidEndpoint;
+	CSphString				m_sRawBody;
+	CSphString				m_sCurField;
+	OptionsHash_t			m_hOptions;
 };
 };
 
 
-HttpRequestParser_t::HttpRequestParser_t ()
-{
-	m_eEndpoint = SPH_HTTP_ENDPOINT_DEFAULT;
-	m_bKeepAlive = false;
-	m_sError = NULL;
-}
 
 
-bool HttpRequestParser_t::Parse ( const CSphVector<BYTE> & dData )
+bool HttpRequestParser_c::Parse ( const BYTE * pData, int iDataLen )
 {
 {
 	http_parser_settings tParserSettings;
 	http_parser_settings tParserSettings;
 	http_parser_settings_init ( &tParserSettings );
 	http_parser_settings_init ( &tParserSettings );
-	tParserSettings.on_url = HttpRequestParser_t::ParserUrl;
-	tParserSettings.on_header_field = HttpRequestParser_t::ParserHeaderField;
-	tParserSettings.on_header_value = HttpRequestParser_t::ParserHeaderValue;
-	tParserSettings.on_body = HttpRequestParser_t::ParserBody;
+	tParserSettings.on_url = HttpRequestParser_c::ParserUrl;
+	tParserSettings.on_header_field = HttpRequestParser_c::ParserHeaderField;
+	tParserSettings.on_header_value = HttpRequestParser_c::ParserHeaderValue;
+	tParserSettings.on_body = HttpRequestParser_c::ParserBody;
 
 
 	http_parser tParser;
 	http_parser tParser;
 	tParser.data = this;
 	tParser.data = this;
 	http_parser_init ( &tParser, HTTP_REQUEST );
 	http_parser_init ( &tParser, HTTP_REQUEST );
-	int iParsed = http_parser_execute ( &tParser, &tParserSettings, (const char *)dData.Begin(), dData.GetLength() );
-
-	if ( iParsed!=dData.GetLength() )
+	int iParsed = http_parser_execute ( &tParser, &tParserSettings, (const char *)pData, iDataLen );
+	if ( iParsed!=iDataLen )
 	{
 	{
-		m_sError = http_errno_description ( (http_errno)tParser.http_errno );
+		m_szError = http_errno_description ( (http_errno)tParser.http_errno );
 		return false;
 		return false;
 	}
 	}
 
 
@@ -397,7 +363,7 @@ static void UriPercentReplace ( CSphString & sEntity )
 }
 }
 
 
 
 
-bool HttpRequestParser_t::ParseList ( const char * sAt, int iLen )
+bool HttpRequestParser_c::ParseList ( const char * sAt, int iLen )
 {
 {
 	const char * sCur = sAt;
 	const char * sCur = sAt;
 	const char * sEnd = sAt + iLen;
 	const char * sEnd = sAt + iLen;
@@ -436,21 +402,8 @@ bool HttpRequestParser_t::ParseList ( const char * sAt, int iLen )
 	return true;
 	return true;
 }
 }
 
 
-static ESphHttpEndpoint ParseEndpointPath ( const char * sPath, int iLen )
-{
-	if ( !iLen )
-		return SPH_HTTP_ENDPOINT_INDEX;
-
-	for ( int i=0; i<SPH_HTTP_ENDPOINT_TOTAL; i++ )
-	{
-		if ( strncmp ( sPath, g_sEndpoints[i], Min ( iLen, (int)strlen ( g_sEndpoints[i] ) ) )==0 )
-			return (ESphHttpEndpoint)i;
-	}
-
-	return SPH_HTTP_ENDPOINT_MISSED;
-}
 
 
-int HttpRequestParser_t::ParserUrl ( http_parser * pParser, const char * sAt, size_t iLen )
+int HttpRequestParser_c::ParserUrl ( http_parser * pParser, const char * sAt, size_t iLen )
 {
 {
 	http_parser_url tUri;
 	http_parser_url tUri;
 	if ( http_parser_parse_url ( sAt, iLen, 0, &tUri ) )
 	if ( http_parser_parse_url ( sAt, iLen, 0, &tUri ) )
@@ -468,42 +421,42 @@ int HttpRequestParser_t::ParserUrl ( http_parser * pParser, const char * sAt, si
 			sPath++;
 			sPath++;
 			iPathLen--;
 			iPathLen--;
 		}
 		}
-		ESphHttpEndpoint eEndpoint = ParseEndpointPath ( sPath, iPathLen );
-		( (HttpRequestParser_t *)pParser->data )->m_eEndpoint = eEndpoint;
-		if ( eEndpoint==SPH_HTTP_ENDPOINT_MISSED )
-		{
-			( (HttpRequestParser_t *)pParser->data )->m_sInvalidEndpoint.SetBinary ( sPath, iPathLen );
-		}
+
+		CSphString sEndpoint ( sPath, iPathLen );
+		ESphHttpEndpoint eEndpoint = sphStrToHttpEndpoint ( sEndpoint );
+		( (HttpRequestParser_c *)pParser->data )->m_eEndpoint = eEndpoint;
+		if ( eEndpoint==SPH_HTTP_ENDPOINT_TOTAL )
+			( (HttpRequestParser_c *)pParser->data )->m_sInvalidEndpoint.SetBinary ( sPath, iPathLen );
 	}
 	}
 
 
 	if ( ( tUri.field_set & uQuery )!=0 )
 	if ( ( tUri.field_set & uQuery )!=0 )
-		( (HttpRequestParser_t *)pParser->data )->ParseList ( sAt + tUri.field_data[UF_QUERY].off, tUri.field_data[UF_QUERY].len );
+		( (HttpRequestParser_c *)pParser->data )->ParseList ( sAt + tUri.field_data[UF_QUERY].off, tUri.field_data[UF_QUERY].len );
 
 
 	return 0;
 	return 0;
 }
 }
 
 
-int HttpRequestParser_t::ParserHeaderField ( http_parser * pParser, const char * sAt, size_t iLen )
+int HttpRequestParser_c::ParserHeaderField ( http_parser * pParser, const char * sAt, size_t iLen )
 {
 {
 	assert ( pParser->data );
 	assert ( pParser->data );
-	( (HttpRequestParser_t *)pParser->data )->m_sCurField.SetBinary ( sAt, iLen );
+	( (HttpRequestParser_c *)pParser->data )->m_sCurField.SetBinary ( sAt, iLen );
 	return 0;
 	return 0;
 }
 }
 
 
-int HttpRequestParser_t::ParserHeaderValue ( http_parser * pParser, const char * sAt, size_t iLen )
+int HttpRequestParser_c::ParserHeaderValue ( http_parser * pParser, const char * sAt, size_t iLen )
 {
 {
 	assert ( pParser->data );
 	assert ( pParser->data );
 	CSphString sVal;
 	CSphString sVal;
 	sVal.SetBinary ( sAt, iLen );
 	sVal.SetBinary ( sAt, iLen );
-	HttpRequestParser_t * pHttpParser = (HttpRequestParser_t *)pParser->data;
+	HttpRequestParser_c * pHttpParser = (HttpRequestParser_c *)pParser->data;
 	pHttpParser->m_hOptions.Add ( sVal, pHttpParser->m_sCurField );
 	pHttpParser->m_hOptions.Add ( sVal, pHttpParser->m_sCurField );
 	pHttpParser->m_sCurField = "";
 	pHttpParser->m_sCurField = "";
 	return 0;
 	return 0;
 }
 }
 
 
-int HttpRequestParser_t::ParserBody ( http_parser * pParser, const char * sAt, size_t iLen )
+int HttpRequestParser_c::ParserBody ( http_parser * pParser, const char * sAt, size_t iLen )
 {
 {
 	assert ( pParser->data );
 	assert ( pParser->data );
-	HttpRequestParser_t * pHttpParser = (HttpRequestParser_t *)pParser->data;
+	HttpRequestParser_c * pHttpParser = (HttpRequestParser_c *)pParser->data;
 	pHttpParser->ParseList ( sAt, iLen );
 	pHttpParser->ParseList ( sAt, iLen );
 	pHttpParser->m_sRawBody.SetBinary ( sAt, iLen );
 	pHttpParser->m_sRawBody.SetBinary ( sAt, iLen );
 	return 0;
 	return 0;
@@ -554,106 +507,12 @@ static const char * g_sIndexPage =
 "</body>"
 "</body>"
 "</html>";
 "</html>";
 
 
-static void HttpHandlerPage ( bool bPage, const CSphString & sInvalidEndpoint, CSphVector<BYTE> & dData )
-{
-	if ( bPage )
-	{
-		StringBuilder_c sIndexPage;
-		sIndexPage.Appendf ( g_sIndexPage, SPHINX_VERSION );
-		HttpBuildReply ( dData, SPH_HTTP_STATUS_200, sIndexPage.cstr(), sIndexPage.Length(), true );
-	} else
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_501, "/%s - unsupported endpoint", sInvalidEndpoint.cstr() );
-	}
-}
-
-struct HttpQuerySettings_t
-{
-	bool		m_bProfile;
-	QueryType_e m_eQueryType;
-	bool		m_bAttrHighlight;
-
-	HttpQuerySettings_t ()
-		: m_bProfile ( false )
-		, m_eQueryType ( QUERY_SQL )
-		, m_bAttrHighlight ( false )
-	{}
-};
 
 
-static QueryParser_i * ParseQuerySql ( CSphQuery & tQuery, const HttpRequestParser_t & tParser, CSphVector<BYTE> & dData, HttpQuerySettings_t & tSettings, CSphString & /*sWarning*/ )
+static void HttpHandlerIndexPage ( CSphVector<BYTE> & dData )
 {
 {
-	const CSphString * pRawQl = tParser.m_hOptions ( "query" );
-	if ( !pRawQl || pRawQl->IsEmpty() )
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, "query missed" );
-		return NULL;
-	}
-
-	CSphString sError;
-	CSphVector<SqlStmt_t> dStmt;
-	if ( !sphParseSqlQuery ( pRawQl->cstr(), pRawQl->Length(), dStmt, sError, SPH_COLLATION_DEFAULT ) )
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return NULL;
-	}
-
-	tQuery = dStmt[0].m_tQuery;
-	if ( dStmt.GetLength()>1 )
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_501, "multiple queries not supported" );
-		return NULL;
-	} else if ( dStmt[0].m_eStmt!=STMT_SELECT )
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_501, "only SELECT queries supported" );
-		return NULL;
-	}
-
-	tSettings.m_eQueryType = QUERY_SQL;
-
-	return sphCreatePlainQueryParser();
-}
-
-
-static QueryParser_i * ParseQueryPlain ( CSphQuery & tQuery, const HttpRequestParser_t & tParser, CSphVector<BYTE> & dData, HttpQuerySettings_t & tSettings, CSphString & /*sWarning*/ )
-{
-	CSphString sError;
-	ParseSearchOptions ( tParser.m_hOptions, tQuery );
-	if ( !tQuery.ParseSelectList ( sError ) )
-	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return NULL;
-	}
-
-	if ( !tQuery.m_sSortBy.IsEmpty() )
-		tQuery.m_eSort = SPH_SORT_EXTENDED;
-
-	tSettings.m_eQueryType = QUERY_SQL;
-
-	return sphCreatePlainQueryParser();
-}
-
-
-static QueryParser_i * ParseQueryJson ( CSphQuery & tQuery, const HttpRequestParser_t & tParser, CSphVector<BYTE> & dData, HttpQuerySettings_t & tSettings, CSphString & sWarning )
-{
-	CSphString sError;
-	if ( !sphParseJsonQuery ( tParser.m_sRawBody.cstr(), tQuery, tSettings.m_bProfile, tSettings.m_bAttrHighlight, sError, sWarning ) )
-	{
-		sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return NULL;
-	}
-
-	tSettings.m_eQueryType = QUERY_JSON;
-
-	return sphCreateJsonQueryParser();
-}
-
-
-void EncodeResultPlain ( const AggrResult_t & tRes, const CSphQuery & , CSphQueryProfile *, bool , CSphString & sResult )
-{
-	JsonEscapedBuilder tResBuilder;
-	EncodeResultJson ( tRes, tResBuilder );
-	sResult.Adopt ( tResBuilder.Leak() );
+	StringBuilder_c sIndexPage;
+	sIndexPage.Appendf ( g_sIndexPage, SPHINX_VERSION );
+	HttpBuildReply ( dData, SPH_HTTP_STATUS_200, sIndexPage.cstr(), sIndexPage.Length(), true );
 }
 }
 
 
 
 
@@ -698,55 +557,100 @@ const char * CSphQueryProfileJson::GetResultAsStr() const
 	return nullptr;
 	return nullptr;
 }
 }
 
 
+//////////////////////////////////////////////////////////////////////////
+class JsonRequestBuilder_c : public IRequestBuilder_t
+{
+public:
+	JsonRequestBuilder_c ( const CSphString & sQuery, const SqlStmt_t & /*tStmt*/, ESphHttpEndpoint eEndpoint )
+		: m_eEndpoint ( eEndpoint )
+	{
+		// fixme: we can implement replacing indexes in a string (without parsing) if it becomes a performance issue
+		m_pQuery = cJSON_Parse ( sQuery.cstr() );
+		assert ( m_pQuery );
+	}
+
+	~JsonRequestBuilder_c()
+	{
+		cJSON_Delete ( m_pQuery );
+	}
+
+	void BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const override
+	{
+		// replace "index" value in the json query
+		cJSON_DeleteItemFromObject ( m_pQuery, "index" );
+		cJSON_AddStringToObject ( m_pQuery, "index", tAgent.m_tDesc.m_sIndexes.cstr() );
+		CSphString sRequest = sphJsonToString ( m_pQuery );
+		CSphString sEndpoint = sphHttpEndpointToStr ( m_eEndpoint );
+
+		tOut.SendWord ( SEARCHD_COMMAND_JSON );
+		tOut.SendWord ( VER_COMMAND_JSON );
+		tOut.SendInt ( sEndpoint.Length() + sRequest.Length() + 8 );
+
+		tOut.SendString ( sEndpoint.cstr() );
+		tOut.SendString ( sRequest.cstr() );
+	}
 
 
+private:
+	cJSON *				m_pQuery {nullptr};
+	ESphHttpEndpoint	m_eEndpoint {SPH_HTTP_ENDPOINT_TOTAL};
+};
 
 
-using ParseQueryFunc_fn = QueryParser_i * (CSphQuery &, const HttpRequestParser_t &, CSphVector<BYTE> &, HttpQuerySettings_t &, CSphString & );
-using ResultEncodeFunc_fn = void ( const AggrResult_t &, const CSphQuery &, CSphQueryProfile *, bool, CSphString & );
 
 
-static void HttpHandlerSearch ( ParseQueryFunc_fn * pParseQueryFunc, ResultEncodeFunc_fn * pResultEncodeFunc, const HttpRequestParser_t & tParser, int iCID, CSphVector<BYTE> & dData )
+class JsonReplyParser_c : public IReplyParser_t
 {
 {
-	assert ( pParseQueryFunc && pResultEncodeFunc );
+public:
+	JsonReplyParser_c ( int & iAffected, int & iWarnings )
+		: m_iAffected ( iAffected )
+		, m_iWarnings ( iWarnings )
+	{}
 
 
-	HttpQuerySettings_t tQuerySettings;
-	CSphQuery tQuery;
-	CSphString sWarning;
-	QueryParser_i * pQueryParser = pParseQueryFunc ( tQuery, tParser, dData, tQuerySettings, sWarning );
-	if ( !pQueryParser )
-		return;
+	virtual bool ParseReply ( MemInputBuffer_c & tReq, AgentConn_t & ) const
+	{
+		CSphString sEndpoint = tReq.GetString();
+		ESphHttpEndpoint eEndpoint = sphStrToHttpEndpoint ( sEndpoint );
+		if ( eEndpoint!=SPH_HTTP_ENDPOINT_JSON_UPDATE && eEndpoint!=SPH_HTTP_ENDPOINT_JSON_DELETE )
+			return false;
 
 
-	CSphQueryProfileJson tProfile;
+		DWORD uLength = tReq.GetDword();
+		CSphFixedVector<BYTE> dResult ( uLength );
+		tReq.GetBytes ( dResult.Begin(), uLength );
 
 
-	tQuery.m_pQueryParser = pQueryParser;
-	CSphScopedPtr<ISphSearchHandler> tHandler ( sphCreateSearchHandler ( 1, pQueryParser, tQuerySettings.m_eQueryType, true, iCID ) );
+		return sphGetResultStats ( (const char *)dResult.Begin(), m_iAffected, m_iWarnings, eEndpoint==SPH_HTTP_ENDPOINT_JSON_UPDATE );
+	}
 
 
-	if ( tQuerySettings.m_bProfile )
-		tHandler->SetProfile ( &tProfile );
+protected:
+	int &	m_iAffected;
+	int &	m_iWarnings;
+};
 
 
-	tHandler->SetQuery ( 0, tQuery );
 
 
-	// search
-	tHandler->RunQueries();
+class JsonParserFactory_c : public QueryParserFactory_i
+{
+public:
+	JsonParserFactory_c ( ESphHttpEndpoint eEndpoint )
+		: m_eEndpoint ( eEndpoint )
+	{}
 
 
-	if ( tQuerySettings.m_bProfile )
-		tProfile.Stop();
+	virtual QueryParser_i * CreateQueryParser() const override
+	{
+		return sphCreateJsonQueryParser();
+	}
 
 
-	AggrResult_t * pRes = tHandler->GetResult ( 0 );
-	if ( !pRes->m_sError.IsEmpty() )
+	IRequestBuilder_t * CreateRequestBuilder ( const CSphString & sQuery, const SqlStmt_t & tStmt ) const override
 	{
 	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_500, pRes->m_sError.cstr() );
-		return;
+		return new JsonRequestBuilder_c ( sQuery, tStmt, m_eEndpoint );
 	}
 	}
 
 
-	// fixme: handle more than one warning at once?
-	if ( pRes->m_sWarning.IsEmpty() )
-		pRes->m_sWarning = sWarning;
-	
-	CSphString sResult;
-	pResultEncodeFunc ( *pRes, tQuery, tQuerySettings.m_bProfile ? &tProfile : NULL, tQuerySettings.m_bAttrHighlight, sResult );
-	HttpBuildReply ( dData, SPH_HTTP_STATUS_200, sResult.cstr(), sResult.Length(), false );
-}
+	IReplyParser_t * CreateReplyParser ( int & iUpdated, int & iWarnings ) const override
+	{
+		return new JsonReplyParser_c ( iUpdated, iWarnings );
+	}
 
 
+private:
+	ESphHttpEndpoint m_eEndpoint {SPH_HTTP_ENDPOINT_TOTAL};
+};
 
 
+//////////////////////////////////////////////////////////////////////////
 class HttpErrorReporter_c : public StmtErrorReporter_i
 class HttpErrorReporter_c : public StmtErrorReporter_i
 {
 {
 public:
 public:
@@ -772,278 +676,602 @@ void HttpErrorReporter_c::Error ( const char * /*sStmt*/, const char * sError, M
 	m_sError = sError;
 	m_sError = sError;
 }
 }
 
 
+//////////////////////////////////////////////////////////////////////////
+// all the handlers for http queries
 
 
-void CreateHttpResponse ( bool bResult, cJSON * pResult, CSphVector<BYTE> & dData )
+class HttpHandler_c
 {
 {
-	assert ( pResult );
+public:
+	HttpHandler_c ( const CSphString & sQuery, int iCID, bool bNeedHttpResponse )
+		: m_sQuery ( sQuery )
+		, m_bNeedHttpResponse ( bNeedHttpResponse )
+	{}
 
 
-	CSphString sResult;
-	sResult.Adopt ( cJSON_Print ( pResult ) );
-	HttpBuildReply ( dData, bResult ? SPH_HTTP_STATUS_200 : SPH_HTTP_STATUS_500, sResult.cstr(), sResult.Length(), false );
-}
+	virtual bool Process () = 0;
+	
+	CSphVector<BYTE> & GetResult()
+	{
+		return m_dData;
+	}
 
 
+protected:
+	const CSphString &	m_sQuery;
+	int					m_iCID {0};
+	bool				m_bNeedHttpResponse {false};
+	CSphVector<BYTE>	m_dData;
 
 
-static bool ProcessInsert ( SqlStmt_t & tStmt, SphDocID_t tDocId, bool bReplace, cJSON * & pResult )
-{
-	CSphSessionAccum tAcc ( false );
-	CSphString sWarning;
-	HttpErrorReporter_c tReporter;
-	sphHandleMysqlInsert ( tReporter, tStmt, bReplace, true, sWarning, tAcc, SPH_COLLATION_DEFAULT );
+	void ReportError ( const char * szError, ESphHttpStatus eStatus )
+	{
+		if ( m_bNeedHttpResponse )
+			HttpErrorReply ( m_dData, eStatus, szError );
+		else
+		{
+			m_dData.Resize ( strlen(szError)+1 );
+			memcpy ( m_dData.Begin(), szError, m_dData.GetLength() );
+		}
+	}
 
 
-	if ( tReporter.IsError() )
-		pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
-	else
-		pResult = sphEncodeInsertResultJson ( tStmt.m_sIndex.cstr(), bReplace, tDocId );
+	void BuildReply ( const CSphString & sResult, ESphHttpStatus eStatus )
+	{
+		if ( m_bNeedHttpResponse )
+			HttpBuildReply ( m_dData, eStatus, sResult.cstr(), sResult.Length(), false );
+		else
+		{
+			m_dData.Resize ( sResult.Length()+1 );
+			memcpy ( m_dData.Begin(), sResult.cstr(), m_dData.GetLength() );
+		}
+	}
+};
 
 
-	return !tReporter.IsError();
-}
+
+class HttpOptionsTraits_c
+{
+protected:
+	const OptionsHash_t & m_tOptions;
+
+	HttpOptionsTraits_c ( const OptionsHash_t & tOptions )
+		: m_tOptions ( tOptions )
+	{}
+};
 
 
 
 
-static void HttpHandlerInsert ( const HttpRequestParser_t & tParser, CSphVector<BYTE> & dData, bool bReplace )
+class HttpSearchHandler_c : public HttpHandler_c, public HttpOptionsTraits_c
 {
 {
-	SqlStmt_t tStmt;
-	SphDocID_t tDocId;
-	CSphString sError;
-	if ( !sphParseJsonInsert ( tParser.m_sRawBody.cstr(), tStmt, tDocId, bReplace, sError ) )
+public:
+	HttpSearchHandler_c ( const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResponse )
+		: HttpHandler_c ( sQuery, iCID, bNeedHttpResponse )
+		, HttpOptionsTraits_c ( tOptions )
+	{}
+
+	virtual bool Process () override
 	{
 	{
-		sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return;
+		CSphQuery tQuery;
+		CSphString sWarning;
+		QueryParser_i * pQueryParser = PreParseQuery();
+		if ( !pQueryParser )
+			return false;
+
+		m_tQuery.m_pQueryParser = pQueryParser;
+		CSphScopedPtr<ISphSearchHandler> tHandler ( sphCreateSearchHandler ( 1, pQueryParser, m_eQueryType, true, m_iCID ) );
+
+		CSphQueryProfileJson tProfile;
+		if ( m_bProfile )
+			tHandler->SetProfile ( &tProfile );
+
+		tHandler->SetQuery ( 0, m_tQuery );
+
+		// search
+		tHandler->RunQueries();
+
+		if ( m_bProfile )
+			tProfile.Stop();
+
+		AggrResult_t * pRes = tHandler->GetResult ( 0 );
+		if ( !pRes->m_sError.IsEmpty() )
+		{
+			ReportError ( pRes->m_sError.cstr(), SPH_HTTP_STATUS_500 );
+			return false;
+		}
+
+		// fixme: handle more than one warning at once?
+		if ( pRes->m_sWarning.IsEmpty() )
+			pRes->m_sWarning = m_sWarning;
+
+		CSphString sResult = EncodeResult ( *pRes, m_bProfile ? &tProfile : NULL );
+		BuildReply ( sResult, SPH_HTTP_STATUS_200 );
+
+		return true;
 	}
 	}
 
 
-	cJSON * pResult = NULL;
-	bool bResult = ProcessInsert ( tStmt, tDocId, bReplace, pResult );
-	CreateHttpResponse ( bResult, pResult, dData );
-	cJSON_Delete ( pResult );
-}
+protected:
+	bool					m_bProfile {false};
+	bool					m_bAttrHighlight {false};
+	QueryType_e				m_eQueryType {QUERY_SQL};
+	CSphQuery				m_tQuery;
+	CSphString				m_sWarning;
+
+	virtual QueryParser_i * PreParseQuery() = 0;
+	virtual CSphString		EncodeResult ( const AggrResult_t & tRes, CSphQueryProfile * pProfile ) = 0;
+};
 
 
 
 
-class JsonParserFactory_c : public QueryParserFactory_i
+class HttpSearchHandler_Plain_c : public HttpSearchHandler_c
 {
 {
 public:
 public:
-	virtual QueryParser_i * Create() const override
+	HttpSearchHandler_Plain_c ( const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResponse )
+		: HttpSearchHandler_c ( sQuery, tOptions, iCID, bNeedHttpResponse )
+	{}
+
+protected:
+	QueryParser_i * PreParseQuery() override
 	{
 	{
-		return sphCreateJsonQueryParser();
+		CSphString sError;
+		ParseSearchOptions ( m_tOptions, m_tQuery );
+		if ( !m_tQuery.ParseSelectList ( sError ) )
+		{
+			ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+			return NULL;
+		}
+
+		if ( !m_tQuery.m_sSortBy.IsEmpty() )
+			m_tQuery.m_eSort = SPH_SORT_EXTENDED;
+
+		m_eQueryType = QUERY_SQL;
+
+		return sphCreatePlainQueryParser();
+	}
+
+	CSphString EncodeResult ( const AggrResult_t & tRes, CSphQueryProfile * /*pProfile*/ ) override
+	{
+		JsonEscapedBuilder tResBuilder;
+		EncodeResultJson ( tRes, tResBuilder );
+		CSphString sResult;
+		sResult.Adopt ( tResBuilder.Leak() );
+		return sResult;
 	}
 	}
 };
 };
 
 
 
 
-static bool ProcessUpdate ( SqlStmt_t & tStmt, SphDocID_t tDocId, int iCID, cJSON * & pResult )
+class HttpSearchHandler_SQL_c : public HttpSearchHandler_Plain_c
 {
 {
-	HttpErrorReporter_c tReporter;
-	CSphString sWarning;
-	JsonParserFactory_c tFactory;
-	sphHandleMysqlUpdate ( tReporter, tFactory, tStmt, "", sWarning, iCID );
-
-	if ( tReporter.IsError() )
-		pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
-	else
-		pResult = sphEncodeUpdateResultJson ( tStmt.m_sIndex.cstr(), tDocId, tReporter.GetAffectedRows() );
+public:
+	HttpSearchHandler_SQL_c ( const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResponse )
+		: HttpSearchHandler_Plain_c ( sQuery, tOptions, iCID, bNeedHttpResponse )
+	{}
 
 
-	return !tReporter.IsError();
-}
+protected:
+	QueryParser_i * PreParseQuery() override
+	{
+		const CSphString * pRawQl = m_tOptions ( "query" );
+		if ( !pRawQl || pRawQl->IsEmpty() )
+		{
+			ReportError ( "query missing", SPH_HTTP_STATUS_400 );
+			return NULL;
+		}
 
 
+		CSphString sError;
+		CSphVector<SqlStmt_t> dStmt;
+		if ( !sphParseSqlQuery ( pRawQl->cstr(), pRawQl->Length(), dStmt, sError, SPH_COLLATION_DEFAULT ) )
+		{
+			ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+			return NULL;
+		}
 
 
-static bool ProcessDelete ( SqlStmt_t & tStmt, SphDocID_t tDocId, int iCID, cJSON * & pResult )
-{
-	CSphSessionAccum tAcc ( false );
-	HttpErrorReporter_c tReporter;
-	CSphString sWarning;
-	JsonParserFactory_c tFactory;
-	sphHandleMysqlDelete ( tReporter, tFactory, tStmt, "", true, tAcc, iCID );
-
-	if ( tReporter.IsError() )
-		pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
-	else
-		pResult = sphEncodeDeleteResultJson ( tStmt.m_sIndex.cstr(), tDocId, tReporter.GetAffectedRows() );
+		m_tQuery = dStmt[0].m_tQuery;
+		if ( dStmt.GetLength()>1 )
+		{
+			ReportError ( "multiple queries not supported", SPH_HTTP_STATUS_501 );
+			return NULL;
+		} else if ( dStmt[0].m_eStmt!=STMT_SELECT )
+		{
+			ReportError ( "only SELECT queries are supported", SPH_HTTP_STATUS_501 );
+			return NULL;
+		}
 
 
-	return !tReporter.IsError();
-}
+		m_eQueryType = QUERY_SQL;
 
 
+		return sphCreatePlainQueryParser();
+	}
+};
 
 
-using ParseRequestFunc_fn = bool ( const char *, SqlStmt_t &, SphDocID_t &, CSphString & );
-using Process_fn = bool ( SqlStmt_t &, SphDocID_t, int, cJSON * & );
 
 
-static void HttpHandlerUpdateDelete ( const HttpRequestParser_t & tParser, int iCID, CSphVector<BYTE> & dData, ParseRequestFunc_fn * pParseRequestFunc, Process_fn * pProcessFunc )
+class HttpHandler_JsonSearch_c : public HttpSearchHandler_c
 {
 {
-	assert ( pParseRequestFunc && pProcessFunc );
+public:	
+	HttpHandler_JsonSearch_c ( const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResponse )
+		: HttpSearchHandler_c ( sQuery, tOptions, iCID, bNeedHttpResponse )
+	{}
 
 
-	SqlStmt_t tStmt;
-	SphDocID_t tDocId = DOCID_MAX;
-	CSphString sError;
-	if ( !pParseRequestFunc ( tParser.m_sRawBody.cstr(), tStmt, tDocId, sError ) )
+	QueryParser_i * PreParseQuery() override
 	{
 	{
-		sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return;
+		CSphString sError;
+		if ( !sphParseJsonQuery ( m_sQuery.cstr(), m_tQuery, m_bProfile, m_bAttrHighlight, sError, m_sWarning ) )
+		{
+			sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
+			ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+			return NULL;
+		}
+
+		m_eQueryType = QUERY_JSON;
+
+		return sphCreateJsonQueryParser();
 	}
 	}
 
 
-	cJSON * pResult = NULL;
-	bool bResult = pProcessFunc ( tStmt, tDocId, iCID, pResult );
-	CreateHttpResponse ( bResult, pResult, dData );
-	cJSON_Delete ( pResult );
-}
+protected:
+	CSphString EncodeResult ( const AggrResult_t & tRes, CSphQueryProfile * pProfile ) override
+	{
+		return sphEncodeResultJson ( tRes, m_tQuery, pProfile, m_bAttrHighlight );
+	}
+};
 
 
 
 
-static void AddResultToBulk ( cJSON * pRoot, CSphString & sStmt, cJSON * pResult )
+class HttpJsonInsertTraits_c
 {
 {
-	assert ( pRoot && pResult );
+protected:
+	bool ProcessInsert ( SqlStmt_t & tStmt, SphDocID_t tDocId, bool bReplace, cJSON * & pResult )
+	{
+		CSphSessionAccum tAcc ( false );
+		CSphString sWarning;
+		HttpErrorReporter_c tReporter;
+		sphHandleMysqlInsert ( tReporter, tStmt, bReplace, true, sWarning, tAcc, SPH_COLLATION_DEFAULT );
 
 
-	cJSON * pItem = cJSON_CreateObject();
-	assert ( pItem );
-	cJSON_AddItemToArray ( pRoot, pItem );
+		if ( tReporter.IsError() )
+			pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
+		else
+			pResult = sphEncodeInsertResultJson ( tStmt.m_sIndex.cstr(), bReplace, tDocId );
 
 
-	cJSON_AddItemToObject( pItem, sStmt.cstr(), pResult );
-}
+		return !tReporter.IsError();
+	}
+};
 
 
 
 
-static void HttpHandlerBulk ( const HttpRequestParser_t & tParser, int iCID, CSphVector<BYTE> & dData )
+class HttpHandler_JsonInsert_c : public HttpHandler_c, public HttpJsonInsertTraits_c
 {
 {
-	if ( !tParser.m_hOptions.Exists ("Content-Type") )
-	{
-		CSphString sError = "Content-Type must be set";
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return;
-	}
+public:
+	HttpHandler_JsonInsert_c ( const CSphString & sQuery, bool bReplace, bool bNeedHttpResponse )
+		: HttpHandler_c ( sQuery, 0, bNeedHttpResponse )
+		, m_bReplace ( bReplace )
+	{}
 
 
-	if ( tParser.m_hOptions["Content-Type"].ToLower() != "application/x-ndjson" )
+	bool Process () override
 	{
 	{
-		CSphString sError = "Content-Type must be application/x-ndjson";
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-		return;
+		SqlStmt_t tStmt;
+		SphDocID_t tDocId = DOCID_MAX;
+		CSphString sError;
+		if ( !sphParseJsonInsert ( m_sQuery.cstr(), tStmt, tDocId, m_bReplace, sError ) )
+		{
+			sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
+			ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+			return false;
+		}
+
+		cJSON * pResult = NULL;
+		bool bResult = ProcessInsert ( tStmt, tDocId, m_bReplace, pResult );
+
+		CSphString sResult = sphJsonToString ( pResult );
+		BuildReply( sResult, bResult ? SPH_HTTP_STATUS_200 : SPH_HTTP_STATUS_500 );
+
+		cJSON_Delete ( pResult );
+		return bResult;
 	}
 	}
 
 
-	cJSON * pRoot = cJSON_CreateObject();
-	cJSON * pItems = cJSON_CreateArray();
-	assert ( pRoot && pItems );
-	cJSON_AddItemToObject ( pRoot, "items", pItems );
+private:
+	bool m_bReplace {false};
+};
 
 
-	char * p = const_cast<char*>(tParser.m_sRawBody.cstr());
 
 
-	bool bResult = false;
-	while ( *p )
+class HttpJsonUpdateTraits_c
+{
+protected:
+	bool ProcessUpdate ( const char * szRawRequest, const SqlStmt_t & tStmt, SphDocID_t tDocId, int iCID, cJSON * & pResult )
 	{
 	{
-		while ( sphIsSpace(*p) )
-			p++;
+		HttpErrorReporter_c tReporter;
+		CSphString sWarning;
+		JsonParserFactory_c tFactory ( SPH_HTTP_ENDPOINT_JSON_UPDATE );
+		sphHandleMysqlUpdate ( tReporter, tFactory, tStmt, szRawRequest, sWarning, iCID );
+
+		if ( tReporter.IsError() )
+			pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
+		else
+			pResult = sphEncodeUpdateResultJson ( tStmt.m_sIndex.cstr(), tDocId, tReporter.GetAffectedRows() );
+
+		return !tReporter.IsError();
+	}
+};
 
 
-		char * szStmt = p;
-		while ( *p && *p!='\r' && *p!='\n' )
-			p++;
 
 
-		if ( p-szStmt==0 )
-			break;
+class HttpHandler_JsonUpdate_c : public HttpHandler_c, HttpJsonUpdateTraits_c
+{
+public:
+	HttpHandler_JsonUpdate_c ( const CSphString & sQuery, int iCID, bool bNeedHttpResponse )
+		: HttpHandler_c ( sQuery, iCID, bNeedHttpResponse )
+	{}
 
 
-		*p++ = '\0';
+	bool Process () override
+	{
 		SqlStmt_t tStmt;
 		SqlStmt_t tStmt;
 		SphDocID_t tDocId = DOCID_MAX;
 		SphDocID_t tDocId = DOCID_MAX;
-		CSphString sStmt;
 		CSphString sError;
 		CSphString sError;
-		if ( !sphParseJsonStatement ( szStmt, tStmt, sStmt, tDocId, sError ) )
+		if ( !ParseQuery ( tStmt, tDocId, sError ) )
 		{
 		{
 			sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
 			sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
-			HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-			cJSON_Delete ( pRoot );
-			return;
+			ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+			return false;
 		}
 		}
 
 
 		cJSON * pResult = NULL;
 		cJSON * pResult = NULL;
-		bResult = false;
+		bool bResult = ProcessQuery ( tStmt, tDocId, pResult );
 
 
-		switch ( tStmt.m_eStmt )
-		{
-		case STMT_INSERT:
-		case STMT_REPLACE:
-			bResult = ProcessInsert ( tStmt, tDocId, tStmt.m_eStmt==STMT_REPLACE, pResult );
-			break;
-
-		case STMT_UPDATE:
-			bResult = ProcessUpdate ( tStmt, tDocId, iCID, pResult );
-			break;
-
-		case STMT_DELETE:
-			bResult = ProcessDelete ( tStmt, tDocId, iCID, pResult );
-			break;
-
-		default:
-			sError = "Unknown statement";
-			HttpErrorReply ( dData, SPH_HTTP_STATUS_400, sError.cstr() );
-			cJSON_Delete ( pRoot );
-			return;
-		}
+		CSphString sResult = sphJsonToString ( pResult );
+		BuildReply( sResult, bResult ? SPH_HTTP_STATUS_200 : SPH_HTTP_STATUS_500 );
+
+		cJSON_Delete ( pResult );
+		return bResult;
+	}
 
 
-		AddResultToBulk ( pItems, sStmt, pResult );
-		
-		// no further than the first error
-		if ( !bResult )
-			break;
+protected:
+	virtual bool ParseQuery ( SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError )
+	{
+		return sphParseJsonUpdate ( m_sQuery.cstr(), tStmt, tDocId, sError );
+	}
 
 
-		while ( sphIsSpace(*p) )
-			p++;
+	virtual bool ProcessQuery ( const SqlStmt_t & tStmt, SphDocID_t tDocId, cJSON * & pResult )
+	{
+		return ProcessUpdate ( m_sQuery.cstr(), tStmt, tDocId, m_iCID, pResult );
 	}
 	}
+};
 
 
-	cJSON_AddBoolToObject ( pRoot, "errors", bResult ? 0 : 1 );
 
 
-	CreateHttpResponse ( bResult, pRoot, dData );
-	cJSON_Delete ( pRoot );
-}
+class HttpJsonDeleteTraits_c
+{
+protected:
+	bool ProcessDelete ( const char * szRawRequest, const SqlStmt_t & tStmt, SphDocID_t tDocId, int iCID, cJSON * & pResult )
+	{
+		CSphSessionAccum tAcc ( false );
+		HttpErrorReporter_c tReporter;
+		CSphString sWarning;
+		JsonParserFactory_c tFactory ( SPH_HTTP_ENDPOINT_JSON_DELETE );
+		sphHandleMysqlDelete ( tReporter, tFactory, tStmt, szRawRequest, true, tAcc, iCID );
+
+		if ( tReporter.IsError() )
+			pResult = sphEncodeInsertErrorJson ( tStmt.m_sIndex.cstr(), tReporter.GetError() );
+		else
+			pResult = sphEncodeDeleteResultJson ( tStmt.m_sIndex.cstr(), tDocId, tReporter.GetAffectedRows() );
+
+		return !tReporter.IsError();
+	}
+};
 
 
 
 
-bool sphLoopClientHttp ( CSphVector<BYTE> & dData, int iCID )
+class HttpHandler_JsonDelete_c : public HttpHandler_JsonUpdate_c, public HttpJsonDeleteTraits_c
 {
 {
-	HttpRequestParser_t tParser;
-	if ( !tParser.Parse ( dData ) )
+public:
+	HttpHandler_JsonDelete_c ( const CSphString & sQuery, int iCID, bool bNeedHttpResponse )
+		: HttpHandler_JsonUpdate_c ( sQuery, iCID, bNeedHttpResponse )
+	{}
+
+protected:
+	bool ParseQuery ( SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError ) override
+	{
+		return sphParseJsonDelete ( m_sQuery.cstr(), tStmt, tDocId, sError );
+	}
+
+	bool ProcessQuery ( const SqlStmt_t & tStmt, SphDocID_t tDocId, cJSON * & pResult ) override
 	{
 	{
-		HttpErrorReply ( dData, SPH_HTTP_STATUS_400, tParser.m_sError );
-		return tParser.m_bKeepAlive;
+		return ProcessDelete ( m_sQuery.cstr(), tStmt, tDocId, m_iCID, pResult );
 	}
 	}
+};
+
+
+class HttpHandler_JsonBulk_c : public HttpHandler_c, public HttpOptionsTraits_c, public HttpJsonInsertTraits_c, public HttpJsonUpdateTraits_c, public HttpJsonDeleteTraits_c
+{
+public:
+	HttpHandler_JsonBulk_c ( const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResponse )
+		: HttpHandler_c ( sQuery, iCID, bNeedHttpResponse )
+		, HttpOptionsTraits_c ( tOptions )
+	{}
 
 
-	switch ( tParser.m_eEndpoint )
+	bool Process () override
+	{
+		if ( !m_tOptions.Exists ("Content-Type") )
+		{
+			ReportError ( "Content-Type must be set", SPH_HTTP_STATUS_400 );
+			return false;
+		}
+
+		if ( m_tOptions["Content-Type"].ToLower() != "application/x-ndjson" )
+		{
+			ReportError ( "Content-Type must be application/x-ndjson", SPH_HTTP_STATUS_400 );
+			return false;
+		}
+
+		cJSON * pRoot = cJSON_CreateObject();
+		cJSON * pItems = cJSON_CreateArray();
+		assert ( pRoot && pItems );
+		cJSON_AddItemToObject ( pRoot, "items", pItems );
+
+		// fixme: we're modifying the original query at this point
+		char * p = const_cast<char*>(m_sQuery.cstr());
+
+		bool bResult = false;
+		while ( *p )
+		{
+			while ( sphIsSpace(*p) )
+				p++;
+
+			char * szStmt = p;
+			while ( *p && *p!='\r' && *p!='\n' )
+				p++;
+
+			if ( p-szStmt==0 )
+				break;
+
+			*p++ = '\0';
+			SqlStmt_t tStmt;
+			SphDocID_t tDocId = DOCID_MAX;
+			CSphString sStmt;
+			CSphString sError;
+			CSphString sQuery;
+			if ( !sphParseJsonStatement ( szStmt, tStmt, sStmt, sQuery, tDocId, sError ) )
+			{
+				sError.SetSprintf( "Error parsing json query: %s", sError.cstr() );
+				ReportError ( sError.cstr(), SPH_HTTP_STATUS_400 );
+				cJSON_Delete ( pRoot );
+				return false;
+			}
+
+			cJSON * pResult = NULL;
+			bResult = false;
+
+			switch ( tStmt.m_eStmt )
+			{
+			case STMT_INSERT:
+			case STMT_REPLACE:
+				bResult = ProcessInsert ( tStmt, tDocId, tStmt.m_eStmt==STMT_REPLACE, pResult );
+				break;
+
+			case STMT_UPDATE:
+				bResult = ProcessUpdate ( sQuery.cstr(), tStmt, tDocId, m_iCID, pResult );
+				break;
+
+			case STMT_DELETE:
+				bResult = ProcessDelete ( sQuery.cstr(), tStmt, tDocId, m_iCID, pResult );
+				break;
+
+			default:
+				ReportError ( "Unknown statement", SPH_HTTP_STATUS_400 );
+				cJSON_Delete ( pRoot );
+				return false;
+			}
+
+			AddResult ( pItems, sStmt, pResult );
+
+			// no further than the first error
+			if ( !bResult )
+				break;
+
+			while ( sphIsSpace(*p) )
+				p++;
+		}
+
+		cJSON_AddBoolToObject ( pRoot, "errors", bResult ? 0 : 1 );
+
+		CSphString sResult = sphJsonToString ( pRoot );
+		BuildReply ( sResult, bResult ? SPH_HTTP_STATUS_200 : SPH_HTTP_STATUS_500 );
+		cJSON_Delete ( pRoot );
+
+		return true;
+	}
+
+private:
+	void AddResult ( cJSON * pRoot, CSphString & sStmt, cJSON * pResult )
+	{
+		assert ( pRoot && pResult );
+
+		cJSON * pItem = cJSON_CreateObject();
+		assert ( pItem );
+		cJSON_AddItemToArray ( pRoot, pItem );
+
+		cJSON_AddItemToObject( pItem, sStmt.cstr(), pResult );
+	}
+};
+
+
+static HttpHandler_c * CreateHttpHandler ( ESphHttpEndpoint eEndpoint, const CSphString & sQuery, const OptionsHash_t & tOptions, int iCID, bool bNeedHttpResonse )
+{
+	switch ( eEndpoint )
 	{
 	{
 	case SPH_HTTP_ENDPOINT_SEARCH:
 	case SPH_HTTP_ENDPOINT_SEARCH:
-		HttpHandlerSearch ( ParseQueryPlain, EncodeResultPlain, tParser, iCID, dData );
-		break;
+		return new HttpSearchHandler_Plain_c ( sQuery, tOptions, iCID, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_SQL:
 	case SPH_HTTP_ENDPOINT_SQL:
-		HttpHandlerSearch ( ParseQuerySql, EncodeResultPlain, tParser, iCID, dData );
-		break;
+		return new HttpSearchHandler_SQL_c ( sQuery, tOptions, iCID, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_JSON_SEARCH:
 	case SPH_HTTP_ENDPOINT_JSON_SEARCH:
-		HttpHandlerSearch ( ParseQueryJson, sphEncodeResultJson, tParser, iCID, dData );
-		break;
+		return new HttpHandler_JsonSearch_c ( sQuery, tOptions, iCID, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_JSON_INDEX:
 	case SPH_HTTP_ENDPOINT_JSON_INDEX:
 	case SPH_HTTP_ENDPOINT_JSON_CREATE:
 	case SPH_HTTP_ENDPOINT_JSON_CREATE:
 	case SPH_HTTP_ENDPOINT_JSON_INSERT:
 	case SPH_HTTP_ENDPOINT_JSON_INSERT:
 	case SPH_HTTP_ENDPOINT_JSON_REPLACE:
 	case SPH_HTTP_ENDPOINT_JSON_REPLACE:
-		HttpHandlerInsert ( tParser, dData, tParser.m_eEndpoint==SPH_HTTP_ENDPOINT_JSON_INDEX || tParser.m_eEndpoint==SPH_HTTP_ENDPOINT_JSON_REPLACE );
-		break;
+		return new HttpHandler_JsonInsert_c ( sQuery, eEndpoint==SPH_HTTP_ENDPOINT_JSON_INDEX || eEndpoint==SPH_HTTP_ENDPOINT_JSON_REPLACE, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_JSON_UPDATE:
 	case SPH_HTTP_ENDPOINT_JSON_UPDATE:
-		HttpHandlerUpdateDelete ( tParser, iCID, dData, sphParseJsonUpdate, ProcessUpdate );
-		break;
+		return new HttpHandler_JsonUpdate_c ( sQuery, iCID, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_JSON_DELETE:
 	case SPH_HTTP_ENDPOINT_JSON_DELETE:
-		HttpHandlerUpdateDelete ( tParser, iCID, dData, sphParseJsonDelete, ProcessDelete );
-		break;
+		return new HttpHandler_JsonDelete_c ( sQuery, iCID, bNeedHttpResonse );
 
 
 	case SPH_HTTP_ENDPOINT_JSON_BULK:
 	case SPH_HTTP_ENDPOINT_JSON_BULK:
-		HttpHandlerBulk ( tParser, iCID, dData );
-		break;
+		return new HttpHandler_JsonBulk_c ( sQuery, tOptions, iCID, bNeedHttpResonse );
 
 
-	case SPH_HTTP_ENDPOINT_INDEX:
-		HttpHandlerPage ( true, tParser.m_sInvalidEndpoint, dData );
+	default:
 		break;
 		break;
+	}
 
 
-	case SPH_HTTP_ENDPOINT_MISSED:
-	default:
-		HttpHandlerPage ( false, tParser.m_sInvalidEndpoint, dData );
+	return nullptr;
+}
+
+
+bool sphProcessHttpQuery ( ESphHttpEndpoint eEndpoint, const CSphString & sQuery, const SmallStringHash_T<CSphString> & tOptions, int iCID, CSphVector<BYTE> & dResult, bool bNeedHttpResponse )
+{
+	CSphScopedPtr<HttpHandler_c> pHandler ( CreateHttpHandler ( eEndpoint, sQuery, tOptions, iCID, bNeedHttpResponse ) );
+	if ( !pHandler.Ptr() )
+		return false;
+
+	pHandler->Process();
+	dResult = std::move ( pHandler->GetResult() );
+	return true;
+}
+
+
+bool sphLoopClientHttp ( const BYTE * pRequest, int iRequestLen, CSphVector<BYTE> & dResult, int iCID )
+{
+	HttpRequestParser_c tParser;
+	if ( !tParser.Parse ( pRequest, iRequestLen ) )
+	{
+		HttpErrorReply ( dResult, SPH_HTTP_STATUS_400, tParser.GetError() );
+		return tParser.GetKeepAlive();
+	}
+
+	ESphHttpEndpoint eEndpoint = tParser.GetEndpoint();
+	if ( !sphProcessHttpQuery ( eEndpoint, tParser.GetBody(), tParser.GetOptions(), iCID, dResult, true ) )
+	{
+		if ( eEndpoint==SPH_HTTP_ENDPOINT_INDEX )
+			HttpHandlerIndexPage ( dResult );
+		else
+		{
+			CSphString sError;
+			sError.SetSprintf ( "/%s - unsupported endpoint", tParser.GetInvalidEndpoint().cstr() );
+			HttpErrorReply ( dResult, SPH_HTTP_STATUS_501, sError.cstr() );
+		}
 	}
 	}
 
 
-	return tParser.m_bKeepAlive;
+	return tParser.GetKeepAlive();
 }
 }
 
 
 
 
 void sphHttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * szError )
 void sphHttpErrorReply ( CSphVector<BYTE> & dData, ESphHttpStatus eCode, const char * szError )
 {
 {
-	HttpErrorReply( dData, eCode, "%s", szError );
+	HttpErrorReply ( dData, eCode, szError );
+}
+
+
+const char * g_dEndpoints[] = { "index.html", "search", "sql", "json/search", "json/index", "json/create", "json/insert", "json/replace", "json/update", "json/delete", "json/bulk" };
+STATIC_ASSERT ( sizeof(g_dEndpoints)/sizeof(g_dEndpoints[0])==SPH_HTTP_ENDPOINT_TOTAL, SPH_HTTP_ENDPOINT_SHOULD_BE_SAME_AS_SPH_HTTP_ENDPOINT_TOTAL );
+
+ESphHttpEndpoint sphStrToHttpEndpoint ( const CSphString & sEndpoint )
+{
+	for ( int i = 0; i < SPH_HTTP_ENDPOINT_TOTAL; i++ )
+		if ( sEndpoint==g_dEndpoints[i] )
+			return ESphHttpEndpoint(i);
+
+	return SPH_HTTP_ENDPOINT_TOTAL;
+}
+
+
+CSphString sphHttpEndpointToStr ( ESphHttpEndpoint eEndpoint )
+{
+	assert ( eEndpoint>=SPH_HTTP_ENDPOINT_INDEX && eEndpoint<SPH_HTTP_ENDPOINT_TOTAL );
+	return g_dEndpoints[eEndpoint];
 }
 }

+ 61 - 19
src/sphinxjsonquery.cpp

@@ -1242,7 +1242,7 @@ static bool ParseIndex ( cJSON * pRoot, SqlStmt_t & tStmt, CSphString & sError )
 		return false;
 		return false;
 	}
 	}
 
 
-	cJSON * pIndex = GetJSONPropertyString ( pRoot, "_index", sError );
+	cJSON * pIndex = GetJSONPropertyString ( pRoot, "index", sError );
 	if ( !pIndex )
 	if ( !pIndex )
 		return false;
 		return false;
 
 
@@ -1258,7 +1258,7 @@ static bool ParseIndexId ( cJSON * pRoot, SqlStmt_t & tStmt, SphDocID_t & tDocId
 	if ( !ParseIndex ( pRoot, tStmt, sError ) )
 	if ( !ParseIndex ( pRoot, tStmt, sError ) )
 		return false;
 		return false;
 
 
-	cJSON * pId = GetJSONPropertyInt ( pRoot, "_id", sError );
+	cJSON * pId = GetJSONPropertyInt ( pRoot, "id", sError );
 	if ( !pId )
 	if ( !pId )
 		return false;
 		return false;
 
 
@@ -1292,7 +1292,7 @@ static bool ParseJsonQueryFilters ( cJSON * pQuery, CSphQuery & tQuery, CSphStri
 	bool bFullscan = !pQuery || ( cJSON_GetArraySize(pQuery)==1 && cJSON_HasObjectItem ( pQuery, "match_all" ) );
 	bool bFullscan = !pQuery || ( cJSON_GetArraySize(pQuery)==1 && cJSON_HasObjectItem ( pQuery, "match_all" ) );
 
 
 	if ( !bFullscan )
 	if ( !bFullscan )
-		tQuery.m_sQuery.Adopt ( cJSON_Print ( pQuery ) );
+		tQuery.m_sQuery = sphJsonToString ( pQuery );
 
 
 	// because of the way sphinxql parsing was implemented
 	// because of the way sphinxql parsing was implemented
 	// we need to parse our query and extract filters now
 	// we need to parse our query and extract filters now
@@ -1461,7 +1461,7 @@ bool ParseJsonInsert ( cJSON * pRoot, SqlStmt_t & tStmt, SphDocID_t & tDocId, bo
 			} else if ( cJSON_IsObject ( pItem ) )
 			} else if ( cJSON_IsObject ( pItem ) )
 			{
 			{
 				tNewValue.m_iType = sphGetTokTypeStr();
 				tNewValue.m_iType = sphGetTokTypeStr();
-				tNewValue.m_sVal = cJSON_Print ( pItem );
+				tNewValue.m_sVal = sphJsonToString ( pItem );
 			} else
 			} else
 			{
 			{
 				sError = "unsupported value type";
 				sError = "unsupported value type";
@@ -1493,7 +1493,7 @@ static bool ParseUpdateDeleteQueries ( cJSON * pRoot, SqlStmt_t & tStmt, SphDocI
 	if ( !ParseIndex ( pRoot, tStmt, sError ) )
 	if ( !ParseIndex ( pRoot, tStmt, sError ) )
 		return false;
 		return false;
 
 
-	cJSON * pId = GetJSONPropertyInt ( pRoot, "_id", sError );
+	cJSON * pId = GetJSONPropertyInt ( pRoot, "id", sError );
 	if ( pId )
 	if ( pId )
 	{
 	{
 		CSphFilterSettings & tFilter = tStmt.m_tQuery.m_dFilters.Add();
 		CSphFilterSettings & tFilter = tStmt.m_tQuery.m_dFilters.Add();
@@ -1508,7 +1508,7 @@ static bool ParseUpdateDeleteQueries ( cJSON * pRoot, SqlStmt_t & tStmt, SphDocI
 	cJSON * pQuery = cJSON_GetObjectItem ( pRoot, "query" );
 	cJSON * pQuery = cJSON_GetObjectItem ( pRoot, "query" );
 	if ( pQuery && pId )
 	if ( pQuery && pId )
 	{
 	{
-		sError = "both \"_id\" and \"query\" specified";
+		sError = "both \"id\" and \"query\" specified";
 		return false;
 		return false;
 	}
 	}
 
 
@@ -1599,7 +1599,7 @@ bool sphParseJsonDelete ( const char * szDelete, SqlStmt_t & tStmt, SphDocID_t &
 }
 }
 
 
 
 
-bool sphParseJsonStatement ( const char * szStmt, SqlStmt_t & tStmt, CSphString & sStmt, SphDocID_t & tDocId, CSphString & sError )
+bool sphParseJsonStatement ( const char * szStmt, SqlStmt_t & tStmt, CSphString & sStmt, CSphString & sQuery, SphDocID_t & tDocId, CSphString & sError )
 {
 {
 	CJsonScopedPtr_c pRoot ( cJSON_Parse ( szStmt ) );
 	CJsonScopedPtr_c pRoot ( cJSON_Parse ( szStmt ) );
 	if ( !pRoot.Ptr() )
 	if ( !pRoot.Ptr() )
@@ -1645,6 +1645,8 @@ bool sphParseJsonStatement ( const char * szStmt, SqlStmt_t & tStmt, CSphString
 		return false;
 		return false;
 	}
 	}
 
 
+	sQuery = sphJsonToString ( pStmt );
+
 	return true;
 	return true;
 }
 }
 
 
@@ -1826,7 +1828,17 @@ static bool NeedToSkipAttr ( const CSphString & sName, const CSphQuery & tQuery
 }
 }
 
 
 
 
-void sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery, CSphQueryProfile * pProfile, bool bAttrsHighlight, CSphString & sResult )
+CSphString sphJsonToString ( const cJSON * pJson )
+{
+	// we can't take this string and just adopt it because we need extra 'gap' bytes at the end
+	char * szResult = cJSON_PrintUnformatted ( pJson );
+	CSphString sResult ( szResult );
+	SafeDeleteArray ( szResult );
+	return sResult;
+}
+
+
+CSphString sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery, CSphQueryProfile * pProfile, bool bAttrsHighlight )
 {
 {
 	CJsonScopedPtr_c pJsonRoot ( cJSON_CreateObject() );
 	CJsonScopedPtr_c pJsonRoot ( cJSON_CreateObject() );
 	cJSON * pRoot = pJsonRoot.Ptr();
 	cJSON * pRoot = pJsonRoot.Ptr();
@@ -1838,12 +1850,7 @@ void sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery,
 		cJSON_AddItemToObject ( pRoot, "error", pError );
 		cJSON_AddItemToObject ( pRoot, "error", pError );
 		cJSON_AddStringToObject ( pError, "type", "Error" );
 		cJSON_AddStringToObject ( pError, "type", "Error" );
 		cJSON_AddStringToObject ( pError, "reason", tRes.m_sError.cstr() );
 		cJSON_AddStringToObject ( pError, "reason", tRes.m_sError.cstr() );
-
-		char * szResult = cJSON_Print ( pRoot );
-		assert ( szResult );
-		sResult.Adopt ( &szResult );
-
-		return;
+		return sphJsonToString ( pRoot );
 	}
 	}
 
 
 	cJSON_AddNumberToObject ( pRoot, "took", tRes.m_iQueryTime );
 	cJSON_AddNumberToObject ( pRoot, "took", tRes.m_iQueryTime );
@@ -1945,12 +1952,11 @@ void sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery,
 			cJSON_AddNullToObject ( pRoot, "profile" );
 			cJSON_AddNullToObject ( pRoot, "profile" );
 	}
 	}
 
 
-	char * szResult = cJSON_Print ( pRoot );
-	sResult.Adopt ( &szResult );
+	return sphJsonToString ( pRoot );
 }
 }
 
 
 
 
-cJSON * sphEncodeInsertResultJson( const char * szIndex, bool bReplace, SphDocID_t tDocId )
+cJSON * sphEncodeInsertResultJson ( const char * szIndex, bool bReplace, SphDocID_t tDocId )
 {
 {
 	cJSON * pRoot = cJSON_CreateObject();
 	cJSON * pRoot = cJSON_CreateObject();
 	assert ( pRoot );
 	assert ( pRoot );
@@ -1964,7 +1970,7 @@ cJSON * sphEncodeInsertResultJson( const char * szIndex, bool bReplace, SphDocID
 }
 }
 
 
 
 
-cJSON * sphEncodeUpdateResultJson( const char * szIndex, SphDocID_t tDocId, int iAffected )
+cJSON * sphEncodeUpdateResultJson ( const char * szIndex, SphDocID_t tDocId, int iAffected )
 {
 {
 	cJSON * pRoot = cJSON_CreateObject();
 	cJSON * pRoot = cJSON_CreateObject();
 	assert ( pRoot );
 	assert ( pRoot );
@@ -2021,6 +2027,42 @@ cJSON * sphEncodeInsertErrorJson ( const char * szIndex, const char * szError )
 }
 }
 
 
 
 
+bool sphGetResultStats ( const char * szResult, int & iAffected, int & iWarnings, bool bUpdate )
+{
+	CJsonScopedPtr_c pJsonRoot ( cJSON_Parse ( szResult ) );
+	if ( !pJsonRoot.Ptr() )
+		return false;
+
+	// no warnings in json results for now
+	iWarnings = 0;
+
+	if ( cJSON_GetObjectItem ( pJsonRoot.Ptr(), "error" ) )
+	{
+		iAffected = 0;
+		return true;
+	}
+
+	// its either update or delete
+	CSphString sError;
+	cJSON * pAffected = GetJSONPropertyInt ( pJsonRoot.Ptr(), bUpdate ? "updated" : "deleted", sError );
+	if ( pAffected )
+	{
+		iAffected = pAffected->valueint;
+		return true;
+	}
+
+	// it was probably a query with an "_id"
+	cJSON * pId = GetJSONPropertyInt ( pJsonRoot.Ptr(), "_id", sError );
+	if ( pId )
+	{
+		iAffected = 1;
+		return true;
+	}
+
+	return false;
+}
+
+
 void AddAccessSpecs ( XQNode_t * pNode, cJSON * pJson, const CSphSchema & tSchema  )
 void AddAccessSpecs ( XQNode_t * pNode, cJSON * pJson, const CSphSchema & tSchema  )
 {
 {
 	assert ( pNode && pJson );
 	assert ( pNode && pJson );
@@ -2270,7 +2312,7 @@ bool ParseSnippet ( cJSON * pSnip, CSphQuery & tQuery, CSphString & sError )
 			sError = "\"highlight_query\" property value should be an object";
 			sError = "\"highlight_query\" property value should be an object";
 			return false;
 			return false;
 		}
 		}
-		sQuery = cJSON_Print ( pQuery );
+		sQuery = sphJsonToString ( pQuery );
 	}
 	}
 
 
 	CSphString sPreTag;
 	CSphString sPreTag;

+ 7 - 2
src/sphinxjsonquery.h

@@ -28,12 +28,17 @@ bool			sphParseJsonQuery ( const char * szQuery, CSphQuery & tQuery, bool & bPro
 bool			sphParseJsonInsert ( const char * szInsert, SqlStmt_t & tStmt, SphDocID_t & tDocId, bool bReplace, CSphString & sError );
 bool			sphParseJsonInsert ( const char * szInsert, SqlStmt_t & tStmt, SphDocID_t & tDocId, bool bReplace, CSphString & sError );
 bool			sphParseJsonUpdate ( const char * szUpdate, SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError );
 bool			sphParseJsonUpdate ( const char * szUpdate, SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError );
 bool			sphParseJsonDelete ( const char * szDelete, SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError );
 bool			sphParseJsonDelete ( const char * szDelete, SqlStmt_t & tStmt, SphDocID_t & tDocId, CSphString & sError );
-bool			sphParseJsonStatement ( const char * szStmt, SqlStmt_t & tStmt, CSphString & sStmt, SphDocID_t & tDocId, CSphString & sError );
-void			sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery, CSphQueryProfile * pProfile, bool bAttrsHighlight, CSphString & sResult );
+bool			sphParseJsonStatement ( const char * szStmt, SqlStmt_t & tStmt, CSphString & sStmt, CSphString & sQuery, SphDocID_t & tDocId, CSphString & sError );
+CSphString		sphJsonToString ( const cJSON * pJson );
+
+CSphString		sphEncodeResultJson ( const AggrResult_t & tRes, const CSphQuery & tQuery, CSphQueryProfile * pProfile, bool bAttrsHighlight );
 cJSON *			sphEncodeInsertResultJson ( const char * szIndex, bool bReplace, SphDocID_t tDocId );
 cJSON *			sphEncodeInsertResultJson ( const char * szIndex, bool bReplace, SphDocID_t tDocId );
 cJSON *			sphEncodeUpdateResultJson ( const char * szIndex, SphDocID_t tDocId, int iAffected );
 cJSON *			sphEncodeUpdateResultJson ( const char * szIndex, SphDocID_t tDocId, int iAffected );
 cJSON *			sphEncodeDeleteResultJson ( const char * szIndex, SphDocID_t tDocId, int iAffected );
 cJSON *			sphEncodeDeleteResultJson ( const char * szIndex, SphDocID_t tDocId, int iAffected );
 cJSON *			sphEncodeInsertErrorJson ( const char * szIndex, const char * szError );
 cJSON *			sphEncodeInsertErrorJson ( const char * szIndex, const char * szError );
+
+bool			sphGetResultStats ( const char * szResult, int & iAffected, int & iWarnings, bool bUpdate );
+
 cJSON *			sphBuildProfileJson ( XQNode_t * pNode, const CSphSchema & tSchema );
 cJSON *			sphBuildProfileJson ( XQNode_t * pNode, const CSphSchema & tSchema );
 void			sphInitCJson();
 void			sphInitCJson();
 
 

Файловите разлики са ограничени, защото са твърде много
+ 9 - 9
test/test_338/model.bin


+ 28 - 28
test/test_338/test.xml

@@ -34,8 +34,8 @@ index test_rt
 <!-- inserts -->
 <!-- inserts -->
 <query endpoint="json/insert">
 <query endpoint="json/insert">
 {
 {
-	"_index":"test_rt",
-	"_id":1,
+	"index":"test_rt",
+	"id":1,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 1,
 	    "int_col" : 1,
@@ -55,8 +55,8 @@ index test_rt
 
 
 <query endpoint="json/insert">
 <query endpoint="json/insert">
 {
 {
-	"_index":"test_rt",
-	"_id":2,
+	"index":"test_rt",
+	"id":2,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 2,
 	    "int_col" : 2,
@@ -69,8 +69,8 @@ index test_rt
 
 
 <query endpoint="json/insert">
 <query endpoint="json/insert">
 {
 {
-	"_index":"test_rt",
-	"_id":3,
+	"index":"test_rt",
+	"id":3,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 3,
 	    "int_col" : 3,
@@ -94,23 +94,23 @@ index test_rt
 <!-- "create"=="insert" -->
 <!-- "create"=="insert" -->
 <query endpoint="json/insert">
 <query endpoint="json/insert">
 {
 {
-	"_index":"test_rt",
-	"_id":3
+	"index":"test_rt",
+	"id":3
 }
 }
 </query>
 </query>
 
 
 <query endpoint="json/create">
 <query endpoint="json/create">
 {
 {
-	"_index":"test_rt",
-	"_id":3
+	"index":"test_rt",
+	"id":3
 }
 }
 </query>
 </query>
 
 
 <!-- "index"=="replace" -->
 <!-- "index"=="replace" -->
 <query endpoint="json/replace">
 <query endpoint="json/replace">
 {
 {
-	"_index":"test_rt",
-	"_id":3,
+	"index":"test_rt",
+	"id":3,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 33,
 	    "int_col" : 33,
@@ -122,8 +122,8 @@ index test_rt
 
 
 <query endpoint="json/index">
 <query endpoint="json/index">
 {
 {
-	"_index":"test_rt",
-	"_id":4,
+	"index":"test_rt",
+	"id":4,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 44,
 	    "int_col" : 44,
@@ -138,8 +138,8 @@ index test_rt
 <!-- update -->
 <!-- update -->
 <query endpoint="json/update">
 <query endpoint="json/update">
 {
 {
-	"_index":"test_rt",
-	"_id":3,
+	"index":"test_rt",
+	"id":3,
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 333,
 	    "int_col" : 333,
@@ -153,8 +153,8 @@ index test_rt
 <!-- update json -->
 <!-- update json -->
 <query endpoint="json/update">
 <query endpoint="json/update">
 {
 {
-	"_index":"test_rt",
-	"_id":1,
+	"index":"test_rt",
+	"id":1,
 	"doc":
 	"doc":
 	{
 	{
 	    "json_col.int" : 111
 	    "json_col.int" : 111
@@ -167,22 +167,22 @@ index test_rt
 <!-- delete -->
 <!-- delete -->
 <query endpoint="json/delete">
 <query endpoint="json/delete">
 {
 {
-	"_index":"test_rt",
-	"_id":3
+	"index":"test_rt",
+	"id":3
 }
 }
 </query>
 </query>
 
 
 <query endpoint="json/delete">
 <query endpoint="json/delete">
 {
 {
-	"_index":"test_rt",
-	"_id":4
+	"index":"test_rt",
+	"id":4
 }
 }
 </query>
 </query>
 
 
 <query endpoint="json/delete">
 <query endpoint="json/delete">
 {
 {
-	"_index":"test_rt",
-	"_id":4
+	"index":"test_rt",
+	"id":4
 }
 }
 </query>
 </query>
 
 
@@ -198,7 +198,7 @@ index test_rt
 
 
 <query endpoint="json/update">
 <query endpoint="json/update">
 {
 {
-	"_index":"test_rt",
+	"index":"test_rt",
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 100,
 	    "int_col" : 100,
@@ -221,7 +221,7 @@ index test_rt
 
 
 <query endpoint="json/update">
 <query endpoint="json/update">
 {
 {
-	"_index":"test_rt",
+	"index":"test_rt",
 	"doc":
 	"doc":
 	{
 	{
 	    "int_col" : 200,
 	    "int_col" : 200,
@@ -237,7 +237,7 @@ index test_rt
 <!-- delete-by-query -->
 <!-- delete-by-query -->
 <query endpoint="json/delete">
 <query endpoint="json/delete">
 {
 {
-	"_index":"test_rt",
+	"index":"test_rt",
 	"query": { "range": { "int_col": { "lte": 100 } } }
 	"query": { "range": { "int_col": { "lte": 100 } } }
 }
 }
 </query>
 </query>
@@ -246,7 +246,7 @@ index test_rt
 
 
 <query endpoint="json/delete">
 <query endpoint="json/delete">
 {
 {
-	"_index":"test_rt",
+	"index":"test_rt",
 	"query": { "match": { "*": "two" } } }
 	"query": { "match": { "*": "two" } } }
 }
 }
 </query>
 </query>

+ 16 - 16
test/test_339/model.bin

@@ -1,17 +1,17 @@
-a:1:{i:0;a:6:{i:0;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:492:"
-{ "insert" : { "_index" : "test_rt", "_id" : 1, "doc": { "int_col" : 1, "flt_col" : 1.1, "content" : "bulk doc one" } } }
-{ "insert" : { "_index" : "test_rt", "_id" : 2, "doc": { "int_col" : 2, "flt_col" : 2.2, "content" : "bulk doc two" } } }
-{ "create" : { "_index" : "test_rt", "_id" : 3, "doc": { "int_col" : 3, "flt_col" : 3.3, "content" : "bulk doc three" } } }
-{ "create" : { "_index" : "test_rt", "_id" : 4, "doc": { "int_col" : 4, "flt_col" : 4.4, "content" : "bulk doc four" } } }
-";s:4:"rows";s:322:"{"items":[{"insert":{"_index":"test_rt","_id":1,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":2,"created":true,"result":"created"}},{"create":{"_index":"test_rt","_id":3,"created":true,"result":"created"}},{"create":{"_index":"test_rt","_id":4,"created":true,"result":"created"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:1;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:54:"{ "index": "test_rt", "query": { "match_all": {} } } }";s:4:"rows";s:347:"{"timed_out":false,"hits":{"total":4,"hits":[{"_id":"1","_score":1,"_source":{"int_col":1,"flt_col":1.100000023841858}},{"_id":"2","_score":1,"_source":{"int_col":2,"flt_col":2.200000047683716}},{"_id":"3","_score":1,"_source":{"int_col":3,"flt_col":3.299999952316284}},{"_id":"4","_score":1,"_source":{"int_col":4,"flt_col":4.400000095367432}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:2;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:237:"
-{ "update" : { "_index" : "test_rt", "doc": { "int_col" : 10 }, "query":{ "range": { "flt_col": { "lt": 2.5 } } } } }
-{ "update" : { "_index" : "test_rt", "doc": { "int_col" : 20 }, "query":{ "range": { "flt_col": { "gt": 2.5 } } } } }
-";s:4:"rows";s:114:"{"items":[{"update":{"_index":"test_rt","updated":2}},{"update":{"_index":"test_rt","updated":2}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:3;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:54:"{ "index": "test_rt", "query": { "match_all": {} } } }";s:4:"rows";s:351:"{"timed_out":false,"hits":{"total":4,"hits":[{"_id":"1","_score":1,"_source":{"int_col":10,"flt_col":1.100000023841858}},{"_id":"2","_score":1,"_source":{"int_col":10,"flt_col":2.200000047683716}},{"_id":"3","_score":1,"_source":{"int_col":20,"flt_col":3.299999952316284}},{"_id":"4","_score":1,"_source":{"int_col":20,"flt_col":4.400000095367432}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:4;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:205:"
-{ "delete" : { "_index" : "test_rt", "_id" : 1 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 2 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 3 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 4 } }
-";s:4:"rows";s:314:"{"items":[{"delete":{"_index":"test_rt","_id":1,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":2,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":3,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":4,"found":true,"result":"deleted"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:5;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:103:"
-{ "delete" : { "_index" : "test_rt", "_id" : 1 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 2 } }
+a:1:{i:0;a:6:{i:0;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:484:"
+{ "insert" : { "index" : "test_rt", "id" : 1, "doc": { "int_col" : 1, "flt_col" : 1.1, "content" : "bulk doc one" } } }
+{ "insert" : { "index" : "test_rt", "id" : 2, "doc": { "int_col" : 2, "flt_col" : 2.2, "content" : "bulk doc two" } } }
+{ "create" : { "index" : "test_rt", "id" : 3, "doc": { "int_col" : 3, "flt_col" : 3.3, "content" : "bulk doc three" } } }
+{ "create" : { "index" : "test_rt", "id" : 4, "doc": { "int_col" : 4, "flt_col" : 4.4, "content" : "bulk doc four" } } }
+";s:4:"rows";s:322:"{"items":[{"insert":{"_index":"test_rt","_id":1,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":2,"created":true,"result":"created"}},{"create":{"_index":"test_rt","_id":3,"created":true,"result":"created"}},{"create":{"_index":"test_rt","_id":4,"created":true,"result":"created"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:1;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:54:"{ "index": "test_rt", "query": { "match_all": {} } } }";s:4:"rows";s:347:"{"timed_out":false,"hits":{"total":4,"hits":[{"_id":"1","_score":1,"_source":{"int_col":1,"flt_col":1.100000023841858}},{"_id":"2","_score":1,"_source":{"int_col":2,"flt_col":2.200000047683716}},{"_id":"3","_score":1,"_source":{"int_col":3,"flt_col":3.299999952316284}},{"_id":"4","_score":1,"_source":{"int_col":4,"flt_col":4.400000095367432}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:2;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:235:"
+{ "update" : { "index" : "test_rt", "doc": { "int_col" : 10 }, "query":{ "range": { "flt_col": { "lt": 2.5 } } } } }
+{ "update" : { "index" : "test_rt", "doc": { "int_col" : 20 }, "query":{ "range": { "flt_col": { "gt": 2.5 } } } } }
+";s:4:"rows";s:114:"{"items":[{"update":{"_index":"test_rt","updated":2}},{"update":{"_index":"test_rt","updated":2}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:3;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:54:"{ "index": "test_rt", "query": { "match_all": {} } } }";s:4:"rows";s:351:"{"timed_out":false,"hits":{"total":4,"hits":[{"_id":"1","_score":1,"_source":{"int_col":10,"flt_col":1.100000023841858}},{"_id":"2","_score":1,"_source":{"int_col":10,"flt_col":2.200000047683716}},{"_id":"3","_score":1,"_source":{"int_col":20,"flt_col":3.299999952316284}},{"_id":"4","_score":1,"_source":{"int_col":20,"flt_col":4.400000095367432}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:4;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:197:"
+{ "delete" : { "index" : "test_rt", "id" : 1 } }
+{ "delete" : { "index" : "test_rt", "id" : 2 } }
+{ "delete" : { "index" : "test_rt", "id" : 3 } }
+{ "delete" : { "index" : "test_rt", "id" : 4 } }
+";s:4:"rows";s:314:"{"items":[{"delete":{"_index":"test_rt","_id":1,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":2,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":3,"found":true,"result":"deleted"}},{"delete":{"_index":"test_rt","_id":4,"found":true,"result":"deleted"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:5;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:99:"
+{ "delete" : { "index" : "test_rt", "id" : 1 } }
+{ "delete" : { "index" : "test_rt", "id" : 2 } }
 ";s:4:"rows";s:176:"{"items":[{"delete":{"_index":"test_rt","_id":1,"found":false,"result":"not found"}},{"delete":{"_index":"test_rt","_id":2,"found":false,"result":"not found"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}}}
 ";s:4:"rows";s:176:"{"items":[{"delete":{"_index":"test_rt","_id":1,"found":false,"result":"not found"}},{"delete":{"_index":"test_rt","_id":2,"found":false,"result":"not found"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}}}

+ 12 - 12
test/test_339/test.xml

@@ -30,31 +30,31 @@ index test_rt
 <httpqueries>
 <httpqueries>
 
 
 <query endpoint="json/bulk" content="application/x-ndjson">
 <query endpoint="json/bulk" content="application/x-ndjson">
-{ "insert" : { "_index" : "test_rt", "_id" : 1, "doc": { "int_col" : 1, "flt_col" : 1.1, "content" : "bulk doc one" } } }
-{ "insert" : { "_index" : "test_rt", "_id" : 2, "doc": { "int_col" : 2, "flt_col" : 2.2, "content" : "bulk doc two" } } }
-{ "create" : { "_index" : "test_rt", "_id" : 3, "doc": { "int_col" : 3, "flt_col" : 3.3, "content" : "bulk doc three" } } }
-{ "create" : { "_index" : "test_rt", "_id" : 4, "doc": { "int_col" : 4, "flt_col" : 4.4, "content" : "bulk doc four" } } }
+{ "insert" : { "index" : "test_rt", "id" : 1, "doc": { "int_col" : 1, "flt_col" : 1.1, "content" : "bulk doc one" } } }
+{ "insert" : { "index" : "test_rt", "id" : 2, "doc": { "int_col" : 2, "flt_col" : 2.2, "content" : "bulk doc two" } } }
+{ "create" : { "index" : "test_rt", "id" : 3, "doc": { "int_col" : 3, "flt_col" : 3.3, "content" : "bulk doc three" } } }
+{ "create" : { "index" : "test_rt", "id" : 4, "doc": { "int_col" : 4, "flt_col" : 4.4, "content" : "bulk doc four" } } }
 </query>
 </query>
 
 
 <query endpoint="json/search">{ "index": "test_rt", "query": { "match_all": {} } } }</query>
 <query endpoint="json/search">{ "index": "test_rt", "query": { "match_all": {} } } }</query>
 
 
 <query endpoint="json/bulk" content="application/x-ndjson">
 <query endpoint="json/bulk" content="application/x-ndjson">
-{ "update" : { "_index" : "test_rt", "doc": { "int_col" : 10 }, "query":{ "range": { "flt_col": { "lt": 2.5 } } } } }
-{ "update" : { "_index" : "test_rt", "doc": { "int_col" : 20 }, "query":{ "range": { "flt_col": { "gt": 2.5 } } } } }
+{ "update" : { "index" : "test_rt", "doc": { "int_col" : 10 }, "query":{ "range": { "flt_col": { "lt": 2.5 } } } } }
+{ "update" : { "index" : "test_rt", "doc": { "int_col" : 20 }, "query":{ "range": { "flt_col": { "gt": 2.5 } } } } }
 </query>
 </query>
 
 
 <query endpoint="json/search">{ "index": "test_rt", "query": { "match_all": {} } } }</query>
 <query endpoint="json/search">{ "index": "test_rt", "query": { "match_all": {} } } }</query>
 
 
 <query endpoint="json/bulk" content="application/x-ndjson">
 <query endpoint="json/bulk" content="application/x-ndjson">
-{ "delete" : { "_index" : "test_rt", "_id" : 1 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 2 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 3 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 4 } }
+{ "delete" : { "index" : "test_rt", "id" : 1 } }
+{ "delete" : { "index" : "test_rt", "id" : 2 } }
+{ "delete" : { "index" : "test_rt", "id" : 3 } }
+{ "delete" : { "index" : "test_rt", "id" : 4 } }
 </query>
 </query>
 
 
 <query endpoint="json/bulk" content="application/x-ndjson">
 <query endpoint="json/bulk" content="application/x-ndjson">
-{ "delete" : { "_index" : "test_rt", "_id" : 1 } }
-{ "delete" : { "_index" : "test_rt", "_id" : 2 } }
+{ "delete" : { "index" : "test_rt", "id" : 1 } }
+{ "delete" : { "index" : "test_rt", "id" : 2 } }
 </query>
 </query>
 
 
 </httpqueries>
 </httpqueries>

+ 34 - 0
test/test_344/model.bin

@@ -0,0 +1,34 @@
+a:1:{i:0;a:10:{i:0;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:475:"
+{ "insert" : { "index" : "test_rt", "id" : 1, "doc": { "tag" : 1, "content" : "doc one" } } }
+{ "insert" : { "index" : "test_rt", "id" : 2, "doc": { "tag" : 2, "content" : "doc two" } } }
+{ "insert" : { "index" : "test_rt", "id" : 3, "doc": { "tag" : 3, "content" : "doc three" } } }
+{ "insert" : { "index" : "test_rt", "id" : 4, "doc": { "tag" : 4, "content" : "doc four" } } }
+{ "insert" : { "index" : "test_rt", "id" : 5, "doc": { "tag" : 5, "content" : "doc five" } } }
+";s:4:"rows";s:396:"{"items":[{"insert":{"_index":"test_rt","_id":1,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":2,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":3,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":4,"created":true,"result":"created"}},{"insert":{"_index":"test_rt","_id":5,"created":true,"result":"created"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:1;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:51:"{ "index": "dist", "query": { "match_all": {} } } }";s:4:"rows";s:262:"{"timed_out":false,"hits":{"total":5,"hits":[{"_id":"1","_score":1,"_source":{"tag":1}},{"_id":"2","_score":1,"_source":{"tag":2}},{"_id":"3","_score":1,"_source":{"tag":3}},{"_id":"4","_score":1,"_source":{"tag":4}},{"_id":"5","_score":1,"_source":{"tag":5}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:2;a:6:{s:13:"http_endpoint";s:11:"json/update";s:11:"http_method";s:4:"POST";s:12:"http_request";s:62:"
+{
+	"index":"dist",
+	"id":1,
+	"doc":
+	{
+	    "tag" : 100
+	}
+}
+";s:4:"rows";s:44:"{"_index":"dist","_id":1,"result":"updated"}";s:9:"http_code";i:200;s:4:"http";i:1;}i:3;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:51:"{ "index": "dist", "query": { "match_all": {} } } }";s:4:"rows";s:264:"{"timed_out":false,"hits":{"total":5,"hits":[{"_id":"1","_score":1,"_source":{"tag":100}},{"_id":"2","_score":1,"_source":{"tag":2}},{"_id":"3","_score":1,"_source":{"tag":3}},{"_id":"4","_score":1,"_source":{"tag":4}},{"_id":"5","_score":1,"_source":{"tag":5}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:4;a:6:{s:13:"http_endpoint";s:11:"json/update";s:11:"http_method";s:4:"POST";s:12:"http_request";s:94:"
+{
+	"index":"dist",
+	"doc":
+	{
+	    "tag" : 200
+	},
+	"query": { "match": { "*": "doc" } } }
+}
+";s:4:"rows";s:29:"{"_index":"dist","updated":5}";s:9:"http_code";i:200;s:4:"http";i:1;}i:5;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:51:"{ "index": "dist", "query": { "match_all": {} } } }";s:4:"rows";s:272:"{"timed_out":false,"hits":{"total":5,"hits":[{"_id":"1","_score":1,"_source":{"tag":200}},{"_id":"2","_score":1,"_source":{"tag":200}},{"_id":"3","_score":1,"_source":{"tag":200}},{"_id":"4","_score":1,"_source":{"tag":200}},{"_id":"5","_score":1,"_source":{"tag":200}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:6;a:6:{s:13:"http_endpoint";s:11:"json/delete";s:11:"http_method";s:4:"POST";s:12:"http_request";s:30:"
+{
+	"index":"dist",
+	"id":1
+}
+";s:4:"rows";s:57:"{"_index":"dist","_id":1,"found":true,"result":"deleted"}";s:9:"http_code";i:200;s:4:"http";i:1;}i:7;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:51:"{ "index": "dist", "query": { "match_all": {} } } }";s:4:"rows";s:227:"{"timed_out":false,"hits":{"total":4,"hits":[{"_id":"2","_score":1,"_source":{"tag":200}},{"_id":"3","_score":1,"_source":{"tag":200}},{"_id":"4","_score":1,"_source":{"tag":200}},{"_id":"5","_score":1,"_source":{"tag":200}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}i:8;a:6:{s:13:"http_endpoint";s:9:"json/bulk";s:11:"http_method";s:4:"POST";s:12:"http_request";s:139:"
+{ "delete" : { "index" : "dist", "id" : 2 } }
+{ "delete" : { "index" : "dist", "id" : 3 } }
+{ "delete" : { "index" : "dist", "id" : 4 } }
+";s:4:"rows";s:233:"{"items":[{"delete":{"_index":"dist","_id":2,"found":true,"result":"deleted"}},{"delete":{"_index":"dist","_id":3,"found":true,"result":"deleted"}},{"delete":{"_index":"dist","_id":4,"found":true,"result":"deleted"}}],"errors":false}";s:9:"http_code";i:200;s:4:"http";i:1;}i:9;a:6:{s:13:"http_endpoint";s:11:"json/search";s:11:"http_method";s:4:"POST";s:12:"http_request";s:51:"{ "index": "dist", "query": { "match_all": {} } } }";s:4:"rows";s:92:"{"timed_out":false,"hits":{"total":1,"hits":[{"_id":"5","_score":1,"_source":{"tag":200}}]}}";s:9:"http_code";i:200;s:4:"http";i:1;}}}

+ 91 - 0
test/test_344/test.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<test>
+<name>distributed json updates</name>
+
+<skip_indexer/>
+
+<requires>
+<http/>
+</requires>
+
+<config>
+searchd
+{
+	<searchd_settings/>
+	binlog_path =
+}
+
+index test_rt
+{
+    type = rt
+    path = <data_path/>/rt
+	rt_field = content
+    rt_attr_uint = tag
+}
+
+index dist
+{
+	type	= distributed
+	agent	= <my_address/>:test_rt
+}
+</config>
+
+<httpqueries>
+
+<query endpoint="json/bulk" content="application/x-ndjson">
+{ "insert" : { "index" : "test_rt", "id" : 1, "doc": { "tag" : 1, "content" : "doc one" } } }
+{ "insert" : { "index" : "test_rt", "id" : 2, "doc": { "tag" : 2, "content" : "doc two" } } }
+{ "insert" : { "index" : "test_rt", "id" : 3, "doc": { "tag" : 3, "content" : "doc three" } } }
+{ "insert" : { "index" : "test_rt", "id" : 4, "doc": { "tag" : 4, "content" : "doc four" } } }
+{ "insert" : { "index" : "test_rt", "id" : 5, "doc": { "tag" : 5, "content" : "doc five" } } }
+</query>
+
+<query endpoint="json/search">{ "index": "dist", "query": { "match_all": {} } } }</query>
+
+<query endpoint="json/update">
+{
+	"index":"dist",
+	"id":1,
+	"doc":
+	{
+	    "tag" : 100
+	}
+}
+</query>
+
+<query endpoint="json/search">{ "index": "dist", "query": { "match_all": {} } } }</query>
+
+<query endpoint="json/update">
+{
+	"index":"dist",
+	"doc":
+	{
+	    "tag" : 200
+	},
+	"query": { "match": { "*": "doc" } } }
+}
+</query>
+
+<query endpoint="json/search">{ "index": "dist", "query": { "match_all": {} } } }</query>
+
+<query endpoint="json/delete">
+{
+	"index":"dist",
+	"id":1
+}
+</query>
+
+<query endpoint="json/search">{ "index": "dist", "query": { "match_all": {} } } }</query>
+
+<query endpoint="json/bulk" content="application/x-ndjson">
+{ "delete" : { "index" : "dist", "id" : 2 } }
+{ "delete" : { "index" : "dist", "id" : 3 } }
+{ "delete" : { "index" : "dist", "id" : 4 } }
+</query>
+
+<query endpoint="json/search">{ "index": "dist", "query": { "match_all": {} } } }</query>
+
+</httpqueries>
+
+</test>

Някои файлове не бяха показани, защото твърде много файлове са промени